320 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
		
		
			
		
	
	
			320 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
|  | <img align="right" src="icon.png"></img> | ||
|  | 
 | ||
|  | # B.S. Logger [](https://travis-ci.org/huafu/bs-logger) [](https://coveralls.io/github/huafu/bs-logger?branch=master) [](https://beerpay.io/huafu/bs-logger)  [](https://beerpay.io/huafu/bs-logger?focus=wish)
 | ||
|  | 
 | ||
|  | **Opinionated bare simple logger for NodeJS (with TypeScript typings)**. | ||
|  | 
 | ||
|  | BSLogger has been created after being disapointed not finding a matching logger on the internet. Not that others aren't good, they just did not fit what I was looking for. | ||
|  | 
 | ||
|  | Here is what I was looking for (and tried to implemented in BSLogger): | ||
|  | - light memory usage | ||
|  | - easily extendable (see `child` method) | ||
|  | - as few dependencies as possible | ||
|  | - ability to define all targets in a string (so that `ENV` vars can be used) | ||
|  | - when using file targets, not re-opening them | ||
|  | - reasonable defautls: | ||
|  |   - logs warnings and above to `stderr` | ||
|  |   - logs JSON to files | ||
|  | - no overhead if it's not going to log anywhere | ||
|  | 
 | ||
|  | ## TL,DR:
 | ||
|  | 
 | ||
|  | Install: | ||
|  | ```sh | ||
|  | npm install --save bs-logger | ||
|  | # or
 | ||
|  | yarn add bs-logger | ||
|  | ``` | ||
|  | Use: | ||
|  | ```js | ||
|  | const { logger } = require('bs-logger'); | ||
|  | //    or | ||
|  | // import logger from 'bs-logger'; | ||
|  | //    or | ||
|  | // import { logger } from 'bs-logger'; | ||
|  | //    as default exports the logger | ||
|  | 
 | ||
|  | logger('foo'); | ||
|  | logger.debug('bar'); | ||
|  | logger.warn({foo: 'bar'}, 'dummy', 'other'/*, ...*/); | ||
|  | ``` | ||
|  | 
 | ||
|  | More complex example: | ||
|  | ```js | ||
|  | // env MY_LOG_TARGETS="debug.log:trace,stderr:warn%json" | ||
|  | import { createLogger } from 'bs-logger'; | ||
|  | const logger = createLogger({ | ||
|  |   context: {namespace: 'http'}, | ||
|  |   targets: process.env.MY_LOG_TARGETS, | ||
|  |   translate: (m) => { | ||
|  |     if (process.env.NODE_ENV === 'production') { | ||
|  |       m.context = { ...m.context, secret: null }; | ||
|  |     } | ||
|  |     return m; | ||
|  |   }, | ||
|  | }); | ||
|  | // [...] | ||
|  | logger.debug({secret: 'xyz'}, 'trying to login') | ||
|  | // will log into debug.log `trying to login` with secret in the context except in prod | ||
|  | 
 | ||
|  | const login = logger.wrap(function login() { | ||
|  |   // your login code | ||
|  | }) | ||
|  | // [...] | ||
|  | login(); | ||
|  | // will log `calling login` with the arguments in context | ||
|  | ``` | ||
|  | 
 | ||
|  | ## Usage
 | ||
|  | 
 | ||
|  | ### Creating a logger
 | ||
|  | 
 | ||
|  | #### Root logger
 | ||
|  | 
 | ||
|  | BSLogger exports a global logger lazyly created on first use, but it is advised to create your own using the `createLogger()` helper: | ||
|  | 
 | ||
|  | - If you are using it in a library wich is meant to be re-distributed: | ||
|  |   ```js | ||
|  |   import { createLogger, LogContexts } 'bs-logger'; | ||
|  |   const logger = createLogger({ [LogContexts.package]: 'my-pacakge' }); | ||
|  |   ``` | ||
|  | 
 | ||
|  | - If you are using it in an application of your own: | ||
|  |   ```js | ||
|  |   import { createLogger, LogContexts } 'bs-logger'; | ||
|  |   const logger = createLogger({ [LogContexts.application]: 'my-app' }); | ||
|  |   ``` | ||
|  | 
 | ||
|  | #### Child logger
 | ||
|  | 
 | ||
|  | Child loggers extends the context, targets and message translators from their parent. You create a child logger using the `child` method: | ||
|  | 
 | ||
|  | ```js | ||
|  | const childLogger = logger.child({ [LogContexts.namespace]: 'http' }) | ||
|  | // childLogger becomes a new logger | ||
|  | ``` | ||
|  | 
 | ||
|  | ### Logging
 | ||
|  | 
 | ||
|  | Any helper to log within BSLogger is a function which has the same signature as `console.log()`, and also accepts an **optional** first argument being the context. A context is any `object`, with some specific (but optional) properties which we'll see later. | ||
|  | 
 | ||
|  | ```ts | ||
|  | logMethod(message: string, ...args: any[]): void | ||
|  |   // or | ||
|  | logMethod(context: LogContext, message: string, ...args: any[]): void | ||
|  | ``` | ||
|  | 
 | ||
|  | #### Directly
 | ||
|  | 
 | ||
|  | You can log using any logger as a function directly (if the logger or its possible parent(s) has not been created with any log level in its context, no level will be attached): | ||
|  | ```js | ||
|  | import { createLogger } from 'bs-logger' | ||
|  | const logger = createLogger() | ||
|  | // [...] | ||
|  | logger('my message'); | ||
|  | ``` | ||
|  | 
 | ||
|  | #### Using level helpers
 | ||
|  | 
 | ||
|  | BSLogger is aware of 6 log levels (`trace`, `debug`, `info`, `warn`, `error` and `fatal`) but you can create your owns. A log level is basically a number. The higher it is, the more important will be the message. You can find log levels constants in `LogLevels` export: | ||
|  | ```js | ||
|  | import { LogLevels } from 'bs-logger'; | ||
|  | 
 | ||
|  | const traceLevelValue = LogLevels.trace; | ||
|  | const debugLevelValue = LogLevels.debug; | ||
|  | // etc. | ||
|  | ``` | ||
|  | 
 | ||
|  | For each log level listed above, a logger will have a helper method to directly log using this level: | ||
|  | ```js | ||
|  | import { createLogger } from 'bs-logger' | ||
|  | const logger = createLogger() | ||
|  | // [...] | ||
|  | logger.trace('foo') | ||
|  | logger.debug('bar') | ||
|  | // etc. | ||
|  | ``` | ||
|  | 
 | ||
|  | Those helpers are the equivalent to | ||
|  | ```js | ||
|  | logger({ [LogContexts.logLevel]: level }, 'foo') | ||
|  | ``` | ||
|  | ...except that they'll be replaced with an empty function on the first call if their level will not be handled by any target. | ||
|  | 
 | ||
|  | ### Wrapping functions
 | ||
|  | 
 | ||
|  | Each logger has a `wrap` method which you can use to wrap a function. If there is no matching log target, the `wrap` method will simply return your function, else it'll wrap it in another function of same signature. The wrapper will, before calling your function, log a message with received arguments in the context. | ||
|  | 
 | ||
|  | ```ts | ||
|  | // With `F` being the type of your funciton: | ||
|  | logger.wrap(func: F): F | ||
|  |   // or | ||
|  | logger.wrap(message: string, func: F): F | ||
|  |   // or | ||
|  | logger.wrap(context: LogContext, messages: string, func: F): F | ||
|  | ``` | ||
|  | 
 | ||
|  | ### Defining target(s)
 | ||
|  | 
 | ||
|  | Each root logger (created using `createLogger` helper) is attached to 0 or more "target". A target is responsible of writing a log entry somewhere. It is an object with the following properties: | ||
|  | 
 | ||
|  | - **minLevel** `string`: The minimum log level this target's strem writer will be called for | ||
|  | - **stream** `{ write: (str: string) => void }`: An object with a write function (like node's `stream.Writable`) which will be used to write log entries | ||
|  | - **format** `(msg: LogMessage) => string`: A formatter which will be used to transform a log entry (message object) into a string | ||
|  | 
 | ||
|  | #### Using targets
 | ||
|  | 
 | ||
|  | When using the global logger, or if no `targets` specified when creating a logger, calling log methods will output to STDERR anything which has log level higher or equal to `warn`. This can be modified as follow by defineing the `LOG_TARGETS` environment variable or passing the `targets` option to `createLogger`. The `targets` can be an array of `LogTarget` (see above) or a `string` defining a list of one or more targets separated by comma (`,`). A `string` target is composed as follow: | ||
|  | - The file path, absolute or relative to CWD. It can also be the specials `stdout` or `stderr` strings (case insensitive). When giving a path to a file, if it ends with the plus sign (`+`) the log data will be appended to the file instead of re-creating the file for each run. | ||
|  | - An optional minimum log level after a colon (`:`). It should be a `number` or the log level name (ie `trace`, `error`, ...). | ||
|  | - An optional formatter name after a percent sign (`%`). There are 2 included formatter: `json` (used for files by default) and `simple` (used for `stdout` and `stderr` by default). See below to define your own. | ||
|  | 
 | ||
|  | Examples: | ||
|  | - `debug.log%simple,stdout:fatal` | ||
|  |   - Log everything to `debug.log` file in CWD dir (re-creates the file for each run). Uses the `simple` formatter. | ||
|  |   - Log only messages with level >= `fatal` to the standard out. | ||
|  | - `errors.log+:error,debug.log:15` | ||
|  |   - Log only messages with level >= `error` to `errors.log` file (without re-creating the file at each run). | ||
|  |   - Log only messages with level >= 15 to `debug.log` file (re-creates the file for each run). | ||
|  | 
 | ||
|  | #### Custom formatters
 | ||
|  | 
 | ||
|  | A custom formatter is a function that takes a `LogMessage` object and returns a `string`. It can be registered giving it a name using the `registerLogFormatter` helper: | ||
|  | 
 | ||
|  | ```js | ||
|  | import { registerLogFormatter, createLogger } from 'bs-logger'; | ||
|  | registerLogFormatter('foo', m => `${m.sequence} ${new Date(m.tim).toLocaleString()} ${m.message}`); | ||
|  | const logger = createLogger({ | ||
|  |   targets: 'stdout%foo', // specifying out formatter | ||
|  |   }); | ||
|  | ``` | ||
|  | 
 | ||
|  | ### Testing
 | ||
|  | 
 | ||
|  | The whole `testing` namespace has useful helpers for using BSLogger while unit testing your product. | ||
|  | 
 | ||
|  | In your tests you would usually prefer not having any logging to happen, or you would like to check what has been logged but without actually logging it to any target. | ||
|  | 
 | ||
|  | The `testing` namespace holds all testing utilities: | ||
|  | ```js | ||
|  | import { testing } from 'bs-logger' | ||
|  | ``` | ||
|  | 
 | ||
|  | - If you use the root logger, here is how to disable its output: | ||
|  | ```js | ||
|  | testing.setup() | ||
|  | ``` | ||
|  | and the `logger` (or `default`) export will become a `LoggerMock` instance (see below). | ||
|  | 
 | ||
|  | - If you create logger(s) using `createLogger`, when testing use the `testing.createLoggerMock` instead. It accepts the same first argument, with an extra second argument, optional, being the `LogTargetMock` to be used (see below). | ||
|  | 
 | ||
|  | #### LoggerMock
 | ||
|  | 
 | ||
|  | Loggers created using the `testing` namespace will have one and only one log target being a `LogTargetMock`, and that target will be set on the `target` extra property of the logger. | ||
|  | 
 | ||
|  | Here are the extra properties of `LogTargetMock` which you can then use for testing: | ||
|  | 
 | ||
|  | - **messages** `LogMessage[]`: all log message objects which would have normally be logged | ||
|  |   - **last** `LogMessage`: the last one being logged | ||
|  |   - **trace** `LogMessage[]`: all log message objects with `trace` level | ||
|  |     - **last** `LogMessage`: last one with `trace` level | ||
|  |   - **debug** `LogMessage[]`: all log message objects with `debug` level | ||
|  |     - **last** `LogMessage`: last one with `debug` level | ||
|  |   - ... | ||
|  | - **lines** `string[]`: all formatted log message lines which would have normally be logged | ||
|  |   - **last** `string`: the last one being logged | ||
|  |   - **trace** `string[]`: all formatted log message lines with `trace` level | ||
|  |     - **last** `string`: last one with `trace` level | ||
|  |   - **debug** `string[]`: all formatted log message lines with `debug` level | ||
|  |     - **last** `string`: last one with `debug` level | ||
|  |   - ... | ||
|  | - **clear** `() => void`: method to clear all log message objects and formatted lines | ||
|  | - **filteredMessages** `(level: number | null, untilLevel?: number) => LogMessage[]`: method to filter log message objects | ||
|  | - **filteredLins** `(level: number | null, untilLevel?: number) => string[]`: method to filter formatted log message lines | ||
|  | 
 | ||
|  | #### Example
 | ||
|  | 
 | ||
|  | Let's say you have a `logger.js` file in which you create the logger for your app: | ||
|  | ```js | ||
|  | // file: logger.js | ||
|  | import { testing, createLogger, LogContexts } from 'bs-logger'; | ||
|  | 
 | ||
|  | const factory = process.env.TEST ? testing.createLoggerMock : createLogger; | ||
|  | 
 | ||
|  | export default factory({ [LogContexts.application]: 'foo' }); | ||
|  | ``` | ||
|  | 
 | ||
|  | In a test you could: | ||
|  | ```js | ||
|  | import logger from './logger'; | ||
|  | // in `fetch(url)` you'd use the logger like `logger.debug({url}, 'GET')` when the request is actually made | ||
|  | import fetch from './http'; | ||
|  | 
 | ||
|  | test('it should cache request', () => { | ||
|  |   logger.target.clear(); | ||
|  |   fetch('http://foo.bar/dummy.json'); | ||
|  |   expect(logger.target.messages.length).toBe(1); | ||
|  |   fetch('http://foo.bar/dummy.json'); | ||
|  |   expect(logger.target.messages.length).toBe(1); | ||
|  |   // you can also expect on the message: | ||
|  |   expect(logger.target.messages.last.message).toBe('GET') | ||
|  |   expect(logger.target.messages.last.context.url).toBe('http://foo.bar/dummy.json') | ||
|  |   // or (mock target formater prefix the message with `[level:xxx] ` when there is a level) | ||
|  |   expect(logger.target.lines.last).toBe('[level:20] GET') | ||
|  |   // or filtering with level: | ||
|  |   expect(logger.target.lines.debug.last).toBe('[level:20] GET') | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | ## Installing
 | ||
|  | 
 | ||
|  | Add to your project with `npm`: | ||
|  | 
 | ||
|  | ```bash | ||
|  | npm install --save bs-logger | ||
|  | ``` | ||
|  | 
 | ||
|  | or with `yarn`: | ||
|  | 
 | ||
|  | ```bash | ||
|  | yarn add bs-logger | ||
|  | ``` | ||
|  | 
 | ||
|  | ## Running the tests
 | ||
|  | 
 | ||
|  | You need to get a copy of the repository to run the tests: | ||
|  | 
 | ||
|  | ```bash | ||
|  | git clone https://github.com/huafu/bs-logger.git | ||
|  | cd bs-logger | ||
|  | npm run test | ||
|  | ``` | ||
|  | 
 | ||
|  | ## Built With
 | ||
|  | 
 | ||
|  | * [TypeScript](https://www.typescriptlang.org/) | ||
|  | * [ts-jest](https://github.com/kulshekhar/ts-jest) | ||
|  | 
 | ||
|  | ## Contributing
 | ||
|  | 
 | ||
|  | Pull requests welcome! | ||
|  | 
 | ||
|  | ## Versioning
 | ||
|  | 
 | ||
|  | We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/huafu/bs-logger/tags). | ||
|  | 
 | ||
|  | ## Authors
 | ||
|  | 
 | ||
|  | * **Huafu Gandon** - *Initial work* - [huafu](https://github.com/huafu) | ||
|  | 
 | ||
|  | See also the list of [contributors](https://github.com/huafu/bs-logger/contributors) who participated in this project. | ||
|  | 
 | ||
|  | ## License
 | ||
|  | 
 | ||
|  | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details | ||
|  | 
 | ||
|  | ## Support on Beerpay
 | ||
|  | Hey dude! Help me out for a couple of :beers:! | ||
|  | 
 | ||
|  | [](https://beerpay.io/huafu/bs-logger)  [](https://beerpay.io/huafu/bs-logger?focus=wish) |