{"id":23507369,"url":"https://github.com/simplyhexagonal/logger","last_synced_at":"2026-05-09T09:45:35.977Z","repository":{"id":71910293,"uuid":"403804731","full_name":"simplyhexagonal/logger","owner":"simplyhexagonal","description":"Extensible debug logger with singleton capabilities, made to easily broadcast to multiple communication channels/transports","archived":false,"fork":false,"pushed_at":"2022-03-09T18:41:51.000Z","size":553,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-05-06T20:36:09.510Z","etag":null,"topics":["debug","hacktoberfest","logger","npm","typescript"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@simplyhexagonal/logger","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/simplyhexagonal.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2021-09-07T01:35:41.000Z","updated_at":"2022-01-21T20:59:55.000Z","dependencies_parsed_at":"2023-06-06T13:01:20.851Z","dependency_job_id":null,"html_url":"https://github.com/simplyhexagonal/logger","commit_stats":{"total_commits":68,"total_committers":2,"mean_commits":34.0,"dds":"0.014705882352941124","last_synced_commit":"9809177ce7318d8eb1c23e1bc604641b0fbf26cf"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simplyhexagonal%2Flogger","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simplyhexagonal%2Flogger/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simplyhexagonal%2Flogger/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simplyhexagonal%2Flogger/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/simplyhexagonal","download_url":"https://codeload.github.com/simplyhexagonal/logger/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252986619,"owners_count":21836196,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["debug","hacktoberfest","logger","npm","typescript"],"created_at":"2024-12-25T10:18:16.788Z","updated_at":"2026-05-09T09:45:35.947Z","avatar_url":"https://github.com/simplyhexagonal.png","language":"TypeScript","funding_links":["https://www.buymeacoffee.com/jeanlescure","https://opencollective.com/simplyhexagonal"],"categories":[],"sub_categories":[],"readme":"# Simply Hexagonal Logger\n![Tests](https://github.com/simplyhexagonal/logger/workflows/tests/badge.svg)\n[![Try logger on RunKit](https://badge.runkitcdn.com/@simplyhexagonal/logger.svg)](https://npm.runkit.com/@simplyhexagonal/logger)\n\nExtensible asynchronous debug logger with singleton capabilities, developed to easily broadcast to\nmultiple communication channels/transports.\n\n```ts\nimport Logger from '@simplyhexagonal/logger';\n\nconst logger = new Logger({});\n\nlogger.debug('Trying to teach', 2, 'tooters', {to: 'toot'});\n```\n\n## Open source notice\n\nThis project is open to updates by its users, [I](https://github.com/jeanlescure) ensure that PRs are relevant to the community.\nIn other words, if you find a bug or want a new feature, please help us by becoming one of the\n[contributors](#contributors-) ✌️ ! See the [contributing section](#contributing)\n\n## Like this module? ❤\n\nPlease consider:\n\n- [Buying me a coffee](https://www.buymeacoffee.com/jeanlescure) ☕\n- Supporting Simply Hexagonal on [Open Collective](https://opencollective.com/simplyhexagonal) 🏆\n- Starring this repo on [Github](https://github.com/simplyhexagonal/logger) 🌟\n\n## Features\n\n- Define different communication channels per log level (i.e. send `debug` messages to console and `error` messages to Slack)\n- Define multiple communication channels per log level\n- Specify channel names and then simply use the `.channel()` function to send messages to any specific channel\n- Extend your logging capabilities with officially supported transports for: Slack, Discord, Email, SMS, Socket\n- 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!)\n- Use the same logger instance throughout your app (singleton logger)\n- Use multiple logger instances throughout your app with de-duplicated communication channels/transports (singleton transports)\n- ANSI colors and easy to read formatting for CLI terminals\n- CSS colors and easy to read formatting for browser dev consoles\n\n## Usage\n\nInstall:\n\n```sh\nnpm install @simplyhexagonal/logger\n\nyarn add @simplyhexagonal/logger\n\npnpm i @simplyhexagonal/logger\n```\n\nThere are three basic configuration elements you should established based on your app's needs and\nthe environment you will deploy to:\n\n- log level\n- communication channels\n- error management strategy\n\n### Log level\n\nThere are 6 log levels:\n\n- debug\n- info\n- warn\n- error\n- fatal\n- all\n\nAnd a bypass level that outputs the message with no date, level, nor colors:\n\n- raw\n\nIn your code you log messages to specific log levels:\n\n```ts\nlogger.debug('hello');\n\ntry {\n  throw new Error('you shall not pass');\n} catch {\n  logger.error('time to turn around');\n}\n\n// 2021-10-02T23:47:27.187Z DEBUG 🐞️:\n//\n//    hello\n//\n\n// 2021-10-02T23:47:27.191Z ERROR 🚨️:\n//\n//    time to turn around\n//\n```\n\nEach log level is given a number value:\n\n```ts\n{\n  debug: 0,\n  info: 10,\n  warn: 20,\n  error: 30,\n  fatal: 40,\n  all: 100,\n  raw: 110,\n}\n```\n\nWhen `Logger` is instantiated, it will only setup communication channels for the configured log\nlevel and above (i.e. if you selected `warn` then the only `logLevel \u003e= 20` would be initialized)\n\nYou can set the log level when instancing the logger:\n\n```ts\nimport Logger, { LogLevels } from '@simplyhexagonal/logger';\n\nnew Logger({\n  logLevel: LogLevels.DEBUG,\n  //...\n});\n```\n\nIt is **highly recommended** to set the log level based on a condition that determines the environment\nyour app is running on:\n\n```ts\nconst logLevel = (process.env.NODE_ENV === 'production') ? LogLevels.ERROR : LogLevels.DEBUG;\n\nnew Logger({\n  logLevel,\n  //...\n});\n```\n\nSetting the log level using environment variables is only recommended as a way to override the\nlog level configured during instantiation:\n\n```sh\n# .env\nLOG_LEVEL=debug\n```\n\n_(i.e. this is useful if you deem it necessary to turn on debug logging in production environments)_\n\n### Communication channels\n\nLet's say that you have a Discord server with a channel you want to receive only debug messages from\nyour app, and another channel dedicated to receiving only errors.\n\nThe debug channel has the webhook path: `/D3BU9/W3BH00K`\n\nThe error channel has the webhook path: `/3RR0R/W3BH00K`\n\nUsing `@simplyhexagonal/logger`, you can add the [official Discord transport](https://www.npmjs.com/package/@simplyhexagonal/logger-transport-discord) as a dependency\nand import it:\n\n```ts\nimport DiscordTransport from '@simplyhexagonal/logger-transport-discord';\n```\n\nThen, you can configure the transports for each of Logger's log levels:\n\n```ts\nconst optionsByLevel = {\n  debug: [\n    {\n      transport: LoggerTransportName.DISCORD,\n      options: {\n        // debug channel webhook url\n        destination: 'https://discord.com/api/webhooks/D3BU9/W3BH00K',\n      },\n    },\n  ],\n  info: [],\n  warn: [],\n  error: [\n    {\n      transport: LoggerTransportName.DISCORD,\n      options: {\n        // error channel webhook url\n        destination: 'https://discord.com/api/webhooks/3RR0R/W3BH00K',\n      },\n    },\n  ],\n  fatal: [],\n  all: [],\n};\n```\n\nThen you would let Logger know which transport to use for `LoggerTransportName.DISCORD`:\n\n```ts\nconst transports = {\n  [LoggerTransportName.DISCORD]: DiscordTransport,\n};\n```\n\nThe final result would look something like this:\n\n```ts\nimport {\n  LogLevels,\n  LoggerTransportName,\n} from '@simplyhexagonal/logger';\nimport DiscordTransport from '@simplyhexagonal/logger-transport-discord';\n\nconst optionsByLevel = {\n  debug: [\n    {\n      transport: LoggerTransportName.DISCORD,\n      options: {\n        // debug channel webhook url\n        destination: 'https://discord.com/api/webhooks/D3BU9/W3BH00K',\n      },\n    },\n  ],\n  info: [],\n  warn: [],\n  error: [\n    {\n      transport: LoggerTransportName.DISCORD,\n      options: {\n        // error channel webhook url\n        destination: 'https://discord.com/api/webhooks/3RR0R/W3BH00K',\n      },\n    },\n  ],\n  fatal: [],\n  all: [],\n  raw: [],\n};\n\nconst transports = {\n  [LoggerTransportName.DISCORD]: DiscordTransport,\n};\n\nconst options = {\n  logLevel: LogLevels.DEBUG,\n  optionsByLevel,\n  transports,\n};\n\nconst logger = new Logger(options);\n```\n\n### Error management strategy\n\nIn the previous example there's always a possibility for the Discord webhook to return an error.\n\nWhen this happens Logger will default to throwing an error which can be handled using `.catch()`:\n\n```ts\nlogger.debug('hello discord').catch((e) =\u003e {\n  logger.channel(LoggerTransportName.CONSOLE).error(e);\n});\n```\n\nWe understand that this adds unnecessary complexity, as such, you are encouraged to turn on error\ncatching when instantiating `Logger`. When you do this, Logger will automagically catch transport\nerrors and log them to console (with `error` log level):\n\n```ts\nconst options = {\n  optionsByLevel: optionsWithBadTransport,\n  catchTransportErrors: true,\n};\n\nconst logger = new Logger(options);\n\nlogger.debug('this will fail due to a bad transport');\n\n//  2021-10-03T04:31:02.191Z ERROR 🚨️:\n//  \n//      {\n//        \"transportResult\": {\n//          \"destination\": \"...\",\n//          \"channelName\": \"...\",\n//          \"error\": {\n//            \"name\": \"Error\",\n//            \"message\": \"LOGGER ERROR: ...\",\n//            \"stack\": \"Error: LOGGER ERROR: ...\",\n//            // ...\n//          },\n//        },\n//        //...\n//      }\n//\n```\n\nFurthermore, you could implement your own fallback transport:\n\n```ts\nimport { LoggerTransport } from '@simplyhexagonal/logger/transports/base';\n\nclass MyTransport extends LoggerTransport {\n  constructor(options: LoggerTransportOptions['options']) {\n    const r = Math.random().toString(36).substring(7);\n    super({...options, r});\n  }\n\n  async error([timestamp, ...message]: unknown[]) {\n    console.log(timestamp, 'MY LOG:', ...message);\n\n    return {\n      destination: this.destination,\n      channelName: this.channelName,\n    };\n  }\n}\n\nconst options = {\n  optionsByLevel: optionsWithBadTransport,\n  catchTransportErrors: true,\n  fallbackTransport: MyTransport,\n};\n\nconst logger = new Logger(options);\n\nlogger.debug('this will fail due to a bad transport');\n\n// 2021-10-03T04:31:02.201Z MY LOG: UndefinedTransportError: ...\n```\n\nAnd just as with `LOG_LEVEL`, we have implemented an environment variable for overriding purposes:\n\n```sh\n# .env\nLOGGER_CATCH_TRANSPORT_ERRORS=true\n```\n\n**IMPORTANT NOTE:** we recommend always setting `catchTransportErrors` to `true` in production!\n\n### More options\n\n```ts\nimport {\n  LogLevels,\n  LoggerTransportName,\n} from '@simplyhexagonal/logger';\nimport DiscordTransport from '@simplyhexagonal/logger-transport-discord';\n\nconst options = {\n  logLevel: LogLevels.DEBUG, // default\n  optionsByLevel: {\n    debug: [\n      // ***\n      // This console config is the default if a log level options array is left empty\n      // (like `info` in this example)\n      {\n        transport: LoggerTransportName.CONSOLE,\n        options: {\n          destination: LoggerTransportName.CONSOLE,\n          channelName: LoggerTransportName.CONSOLE,\n        },\n      },\n      // if you do this you would have only one instance of this transport since all\n      // transports are singleton (as in pre-filtered and de-duplicated)\n      // ***\n      {\n        transport: LoggerTransportName.DISCORD,\n        options: {\n          destination: 'https://discord.com/api/webhooks/D3BU9/W3BH00K',\n          channelName: 'discord-debug',\n        },\n      },\n    ],\n    info: [], // in this case `loggger.info()` will default to logging to the console\n    warn: [],\n    error: [\n      {\n        transport: LoggerTransportName.DISCORD,\n        options: {\n          destination: 'https://discord.com/api/webhooks/3RR0R/W3BH00K',\n        },\n      },\n    ],\n    fatal: [],\n    all: [],\n    raw: [],\n  },\n  transports: {\n    [`${LoggerTransportName.DISCORD}`]: DiscordTransport,\n  },\n  singleton: true, // default\n  catchTransportErrors: false, // default\n  fallbackTransport: MyTransport,\n};\n\nconst logger = new Logger(options);\n```\n\n## The `all` and `raw` log levels\n\nAn important thing to note is that transports defined for the `all` and `raw` log levels will\n**always** be instantiated.\n\nIn the same way, calls to `logger.all()` or `logger.raw()` will **always** log.\n\nFor this reason we suggest only ever using `logger.all()` when an app starts and when an app is\nmanually stopped, and only use `logger.raw()` when outputting messages you absolutely **need** to\nbe unformatted.\n\n## Channels\n\nIt can be extremely useful to setup multiple channels for specific purposes on a log level:\n\n```ts\nconst logger = new Logger({\n  logLevel: LogLevels.DEBUG,\n  optionsByLevel: {\n    warn: [],\n    info: [],\n    debug: [],\n    error: [\n      {\n        transport: LoggerTransportName.SLACK,\n        options: {\n          destination: 'https://hooks.slack.com/services/T123/B456/M0N90',\n          channelName: 'mongo',\n        },\n      },\n      {\n        transport: LoggerTransportName.SLACK,\n        options: {\n          destination: 'https://hooks.slack.com/services/T123/B456/M55QL',\n          channelName: 'mssql',\n        },\n      },\n    ],\n    fatal: [],\n    all: [],\n  },\n});\n```\n\nand then send messages to a specific channel depending on the event that's triggering the log:\n\n```ts\nconst server = async () =\u003e {\n  await mongoose.connect('mongodb://mymongo.cluster:27017/myapp').catch((e) =\u003e {\n    logger.channel('mongo').error(e);\n  });\n\n  await sql.connect('Server=mymssql.cluster,1433;Database=myapp;').catch((e) =\u003e {\n    logger.channel('mssql').error(e);\n  });\n}\n\nserver();\n```\n\n## Transports\n\nWe have the following officially supported transports:\n\n- [WebSocket]() (Coming soon)\n- [Discord](https://www.npmjs.com/package/@simplyhexagonal/logger-transport-discord)\n- [Slack]() (Coming soon)\n- [Email]() (Coming soon)\n- [SMS]() (Coming soon)\n\n## Time functions to measure performance\n\nWe have implemented functions similar to `console.time()` and `console.timeEnd()` to measure performance:\n\n```ts\nconst logger = new Logger({});\n\nlogger.time('my-timed-operation');\n\n// do things\n\nconst result: number = await logger.timeEnd('my-timed-operation');\n\n// transport output:\n//\n//   my-timed-operation: 123.456 ms\n//\n\nconsole.log(result);\n\n// 123.456\n```\n\nThree important differences from the console functions to note:\n\n- `logger.timeEnd` is async as it logs to the transports through the `.raw()` method\n- `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\n- 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(() =\u003e {}, 100)` might be measured to take `99.83 ms` which can break a test if you expect something like `await logger.timeEnd('...') \u003e 100`)\n\n## Contributing\n\nYes, thank you! This plugin is community-driven, most of its features are from different authors.\nPlease update the docs and tests and add your name to the `package.json` file.\n\n## Contributors ✨\n\nThanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --\u003e\n\u003c!-- prettier-ignore-start --\u003e\n\u003c!-- markdownlint-disable --\u003e\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://jeanlescure.cr\"\u003e\u003cimg src=\"https://avatars2.githubusercontent.com/u/3330339?v=4\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eJean Lescure\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"#maintenance-jeanlescure\" title=\"Maintenance\"\u003e🚧\u003c/a\u003e \u003ca href=\"https://github.com/simplyhexagonal/logger/commits?author=jeanlescure\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"#userTesting-jeanlescure\" title=\"User Testing\"\u003e📓\u003c/a\u003e \u003ca href=\"https://github.com/simplyhexagonal/logger/commits?author=jeanlescure\" title=\"Tests\"\u003e⚠️\u003c/a\u003e \u003ca href=\"#example-jeanlescure\" title=\"Examples\"\u003e💡\u003c/a\u003e \u003ca href=\"https://github.com/simplyhexagonal/logger/commits?author=jeanlescure\" title=\"Documentation\"\u003e📖\u003c/a\u003e\u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/2huBrulee\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/29010617?v=4\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eAlejandro Merino\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"#maintenance-2huBrulee\" title=\"Maintenance\"\u003e🚧\u003c/a\u003e \u003ca href=\"https://github.com/simplyhexagonal/logger/commits?author=2huBrulee\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"#userTesting-2huBrulee\" title=\"User Testing\"\u003e📓\u003c/a\u003e \u003ca href=\"https://github.com/simplyhexagonal/logger/commits?author=2huBrulee\" title=\"Tests\"\u003e⚠️\u003c/a\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n\u003c!-- markdownlint-enable --\u003e\n\u003c!-- prettier-ignore-end --\u003e\n\u003c!-- ALL-CONTRIBUTORS-LIST:END --\u003e\n## License\n\nCopyright (c) 2021-Present [Logger Contributors](https://github.com/simplyhexagonal/logger/#contributors-).\u003cbr/\u003e\nLicensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimplyhexagonal%2Flogger","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsimplyhexagonal%2Flogger","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimplyhexagonal%2Flogger/lists"}