├── .commitlintrc.yml ├── .eslintrc.yml ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── docs └── images │ ├── example_context.gif │ ├── example_context.png │ ├── example_data.gif │ ├── example_data.png │ ├── example_pretty.gif │ ├── example_pretty.png │ ├── example_usage1.gif │ ├── example_usage1.png │ ├── example_usage2.gif │ ├── example_usage2.png │ ├── example_usage3.gif │ └── example_usage3.png ├── examples ├── example1.js ├── example2.js ├── example3.js ├── example_context.js ├── example_data.js ├── example_force.js └── example_pretty.js ├── package.json ├── src ├── definitions.ts ├── index.ts ├── output_adapters.ts └── output_utils.ts ├── test ├── logger.js ├── output_adapters.js └── output_utils.js ├── tsconfig.json ├── tsconfig.lib.json └── yarn.lock /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | type-enum: 3 | - 2 4 | - always 5 | - ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert'] 6 | subject-empty: 7 | - 2 8 | - never 9 | subject-max-length: 10 | - 2 11 | - always 12 | - 80 13 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | ecmaVersion: 11 3 | sourceType: script 4 | env: 5 | node: true 6 | es6: true 7 | extends: eslint:recommended 8 | rules: 9 | no-unused-vars: off 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [14.x, 16.x, 18.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | 21 | - name: Get yarn cache directory path 22 | id: yarn-cache-dir-path 23 | run: echo "::set-output name=dir::$(yarn config get cacheFolder)" 24 | 25 | - uses: actions/cache@v3 26 | id: yarn-cache 27 | with: 28 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 29 | key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }} 30 | restore-keys: ${{ runner.os }}-yarn- 31 | 32 | - name: Install dependencies 33 | if: steps.yarn-cache-dir-path.outputs.cache-hit != 'true' 34 | run: yarn install 35 | 36 | - name: Eslint 37 | run: yarn lint 38 | 39 | - name: Prettier 40 | run: yarn check-fmt 41 | 42 | - run: yarn test-cover 43 | env: 44 | CI: true 45 | 46 | - name: Coveralls Parallel 47 | uses: coverallsapp/github-action@v1.1.2 48 | with: 49 | github-token: ${{ secrets.GITHUB_TOKEN }} 50 | flag-name: node-${{ matrix.node-version }} 51 | parallel: true 52 | 53 | coverall: 54 | needs: tests 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: Coveralls Finished 58 | uses: coverallsapp/github-action@v1.1.2 59 | with: 60 | github-token: ${{ secrets.github_token }} 61 | parallel-finished: true -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | strategy: 6 | description: Valid semver number or strategy 7 | default: "patch" 8 | required: false 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [18.x] 17 | 18 | if: ${{ github.actor == 'pebie' || github.actor == 'Crow-EH' || github.actor == 'fthouraud' || github.actor == 'leguellec' || github.actor == 'rande' }} 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Get yarn cache directory path 27 | id: yarn-cache-dir-path 28 | run: echo "::set-output name=dir::$(yarn config get cacheFolder)" 29 | 30 | - uses: actions/cache@v3 31 | id: yarn-cache 32 | with: 33 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 34 | key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }} 35 | restore-keys: ${{ runner.os }}-yarn- 36 | 37 | - name: Install dependencies 38 | if: steps.yarn-cache-dir-path.outputs.cache-hit != 'true' 39 | run: yarn install 40 | 41 | - name: Build 42 | run: yarn build 43 | 44 | - name: Bump the version using input strategy 45 | run: yarn version --new-version ${{ github.event.inputs.strategy }} --no-git-tag-version 46 | 47 | - name: Update changelog 48 | id: changelog 49 | run: | 50 | CHANGELOG=$(yarn conventional-changelog -p conventionalcommits -r -u 0) 51 | echo -e "${CHANGELOG}\n\n\n\n$(cat CHANGELOG.md)" > CHANGELOG.md 52 | BODY=$(echo -e "${CHANGELOG}" | sed -e "1,2d") 53 | BODY="${BODY//'%'/'%25'}" 54 | BODY="${BODY//$'\n'/'%0A'}" 55 | BODY="${BODY//$'\r'/'%0D'}" 56 | echo "::set-output name=body::${BODY}" 57 | 58 | - name: Log changes 59 | run: | 60 | echo "The changelog will be : ${{ steps.changelog.outputs.body }}" 61 | 62 | - name: Get version 63 | id: package-version 64 | uses: martinbeentjes/npm-get-version-action@v1.2.3 65 | 66 | - name: Create tag 67 | run: | 68 | git config user.name github-actions[bot] 69 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 70 | git add . 71 | git commit -m "chore(bump): release v${{ steps.package-version.outputs.current-version }}" 72 | git push 73 | git tag -a v${{ steps.package-version.outputs.current-version }} -m "chore(tag): release v${{ steps.package-version.outputs.current-version }}" 74 | git push origin v${{ steps.package-version.outputs.current-version }} 75 | 76 | - name: Create release 77 | uses: softprops/action-gh-release@v1 78 | with: 79 | body: ${{ steps.changelog.outputs.body }} 80 | tag_name: v${{ steps.package-version.outputs.current-version }} 81 | name: v${{ steps.package-version.outputs.current-version }} 82 | 83 | - name: Setup npmrc 84 | uses: actions/setup-node@v3 85 | with: 86 | node-version: '18.x' 87 | registry-url: 'https://registry.npmjs.org' 88 | 89 | - name: Publish to NPM 90 | run: npm publish --access public 91 | env: 92 | VERSION: ${{ steps.package-version.outputs.current-version }} 93 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | .coveralls.yml 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | build/ 30 | lib/ 31 | bin/ 32 | 33 | # Dependency directories 34 | node_modules 35 | jspm_packages 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | .idea 44 | .vscode 45 | 46 | 47 | # Yarn 2 48 | .yarn/* 49 | !.yarn/releases 50 | !.yarn/plugins 51 | !.yarn/sdks 52 | !.yarn/versions 53 | .pnp.* 54 | .vscode 55 | 56 | events.json -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn check-fmt 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekino/node-logger/962984800b12912447da49d8d7917872775827fe/.npmignore -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 140 2 | semi: false 3 | tabWidth: 4 4 | singleQuote: true 5 | bracketSpacing: true 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | yarn run v1.22.19 2 | $ /home/runner/work/node-logger/node-logger/node_modules/.bin/conventional-changelog -p conventionalcommits -r -u 0 3 | ### [2.1.1](https://github.com/ekino/node-logger/compare/v2.1.0...v2.1.1) (2023-01-05) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * **force:** prevent syncLogger reset canForceWrite ([e8546f1](https://github.com/ekino/node-logger/commit/e8546f112cd8f089825f5a242b08c2aedb393553)) 9 | 10 | Done in 0.52s. 11 | 12 | 13 | 14 | yarn run v1.22.19 15 | $ /home/runner/work/node-logger/node-logger/node_modules/.bin/conventional-changelog -p conventionalcommits -r -u 0 16 | ## [2.1.0](https://github.com/ekino/node-logger/compare/v2.0.2...v2.1.0) (2023-01-04) 17 | 18 | 19 | ### Features 20 | 21 | * **forceWrite:** add posibility to force the log ([017db9e](https://github.com/ekino/node-logger/commit/017db9ea5dc81a536840a300adeea3e2b3f4eb83)) 22 | 23 | Done in 0.41s. 24 | 25 | 26 | 27 | yarn run v1.22.19 28 | $ /home/runner/work/node-logger/node-logger/node_modules/.bin/conventional-changelog -p conventionalcommits -r -u 0 29 | ### [2.0.2](https://github.com/ekino/node-logger/compare/v2.0.1...v2.0.2) (2022-11-10) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * **postinstall:** remove husky postinstall ([e4a53d2](https://github.com/ekino/node-logger/commit/e4a53d28b4378a3bbc92445e8acdbc09d7034234)) 35 | 36 | Done in 0.51s. 37 | 38 | 39 | 40 | yarn run v1.22.19 41 | $ /home/runner/work/node-logger/node-logger/node_modules/.bin/conventional-changelog -p conventionalcommits -r -u 0 42 | ### [2.0.1](https://github.com/ekino/node-logger/compare/v2.0.0...v2.0.1) (2022-11-10) 43 | 44 | Done in 0.37s. 45 | 46 | 47 | 48 | yarn run v1.22.19 49 | $ /home/runner/work/node-logger/node-logger/node_modules/.bin/conventional-changelog -p conventionalcommits -r -u 0 50 | ## [2.0.0](https://github.com/ekino/node-logger/compare/v1.0.0...v2.0.0) (2022-11-10) 51 | 52 | 53 | ### Features 54 | 55 | * **ci:** add release ([67b3b7b](https://github.com/ekino/node-logger/commit/67b3b7baa2b260695d35227d1374aecb145b4622)) 56 | * **typescript:** convert core files to ts ([840741e](https://github.com/ekino/node-logger/commit/840741e296714e4022de8e33a58e3495f6789d3f)) 57 | 58 | Done in 0.50s. 59 | 60 | 61 | 62 | ## [1.0.0](https://github.com/ekino/node-logger/compare/v0.3.0...v1.0.0) (2020-03-25) 63 | 64 | 65 | ### Features 66 | 67 | * **doc:** add missing documentation about log level env ([f071ca4](https://github.com/ekino/node-logger/commit/f071ca4ec160350f25c5dff4dec044d92c77d47e)) 68 | * **logLevel:** override default log level with env variable ([5d207f4](https://github.com/ekino/node-logger/commit/5d207f4aa4ae2da220137a1753ea9967b2ce12d3)) 69 | 70 | ## [0.3.0](https://github.com/ekino/node-logger/compare/v0.2.0...v0.3.0) (2019-12-18) 71 | 72 | 73 | ### Features 74 | 75 | * **ci:** replace flowdock notification with slack ([d5e281c](https://github.com/ekino/node-logger/commit/d5e281c413b30fa26ac8767312be4893637a1d06)) 76 | * **modules:** replace default export with a named function ([c868c6d](https://github.com/ekino/node-logger/commit/c868c6d00d35155fb3c60b65cd8598392fc78b7a)) 77 | * **output:** Support multiple outputs ([3e598f6](https://github.com/ekino/node-logger/commit/3e598f62acbbd39f9a5e93f9e322ff981690a830)) 78 | * **publish:** prevent publication of examples ([1251c4d](https://github.com/ekino/node-logger/commit/1251c4d6247fc78fc3fd9228a1fbf6ca15038f20)) 79 | * **typescript:** add typescript definitions ([#8](https://github.com/ekino/node-logger/issues/8)) ([9be3a9b](https://github.com/ekino/node-logger/commit/9be3a9bf7906c33408875a7ade86442729284176)) 80 | 81 | 82 | ### Bug Fixes 83 | 84 | * **typescript:** fix typescript declarations ([3b470fa](https://github.com/ekino/node-logger/commit/3b470fab9de160c098665f3aceab220fff5a4711)) 85 | 86 | ## [0.2.0](https://github.com/ekino/node-logger/compare/686724b43bc398c9ddf6fcd62ab515c377e4cb51...v0.2.0) (2017-07-05) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * **global-context:** prevent mutation of global context ([686724b](https://github.com/ekino/node-logger/commit/686724b43bc398c9ddf6fcd62ab515c377e4cb51)) 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ekino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | ############## Vars that shouldn't be edited ############## 4 | NODE_MODULES ?= "./node_modules" 5 | NODE_MODULES_BIN ?= "${NODE_MODULES}/.bin" 6 | 7 | FROM_VERSION ?= $(shell yarn run -s version) 8 | EXAMPLE_FILES := $(shell find "examples" -name "*.js") 9 | 10 | ############## HELP ############## 11 | 12 | #COLORS 13 | RED := $(shell tput -Txterm setaf 1) 14 | GREEN := $(shell tput -Txterm setaf 2) 15 | WHITE := $(shell tput -Txterm setaf 7) 16 | YELLOW := $(shell tput -Txterm setaf 3) 17 | RESET := $(shell tput -Txterm sgr0) 18 | 19 | # Add the following 'help' target to your Makefile 20 | # And add help text after each target name starting with '\#\#' 21 | # A category can be added with @category 22 | HELP_HELPER = \ 23 | %help; \ 24 | while(<>) { push @{$$help{$$2 // 'options'}}, [$$1, $$3] if /^([a-zA-Z\-\%]+)\s*:.*\#\#(?:@([a-zA-Z\-\%]+))?\s(.*)$$/ }; \ 25 | print "usage: make [target]\n\n"; \ 26 | for (sort keys %help) { \ 27 | print "${WHITE}$$_:${RESET}\n"; \ 28 | for (@{$$help{$$_}}) { \ 29 | $$sep = " " x (32 - length $$_->[0]); \ 30 | print " ${YELLOW}$$_->[0]${RESET}$$sep${GREEN}$$_->[1]${RESET}\n"; \ 31 | }; \ 32 | print "\n"; } 33 | 34 | help: ##prints help 35 | @perl -e '$(HELP_HELPER)' $(MAKEFILE_LIST) 36 | 37 | ############## RELEASE ############## 38 | changelog: ##@release create changelog 39 | @echo "${YELLOW}generating changelog from v${FROM} to v${RELEASE_VERSION}${RESET}" 40 | ifeq ($(FROM), false) 41 | @yarn run changelog -- -t false 42 | else 43 | @yarn run changelog -- -t "v${FROM}" 44 | endif 45 | 46 | update-package-version: ##@release updates version in package.json 47 | @echo "${YELLOW}updating package.json version to ${RELEASE_VERSION}${RESET}" 48 | @npm version --no-git-tag-version "${RELEASE_VERSION}" 49 | 50 | release: ##@release generates a new release 51 | @echo "${YELLOW}building release ${RELEASE_VERSION} from ${FROM_VERSION}${RESET}" 52 | @-git stash 53 | @make update-package-version 54 | @make changelog 55 | @git add package.json CHANGELOG.md 56 | @git commit -m "chore(v${RELEASE_VERSION}): bump version to ${RELEASE_VERSION}" 57 | @git tag -a "v${RELEASE_VERSION}" -m "version ${RELEASE_VERSION}" 58 | @git push origin v${RELEASE_VERSION} 59 | 60 | examples: ##@examples run examples files 61 | @echo "${YELLOW}start to run all examples files" 62 | @if [ ! -d "lib" ]; then yarn build; fi;\ 63 | for file in $(EXAMPLE_FILES); do echo "\n\nRun $${file}"; node $${file}; done;\ 64 | 65 | .PHONY: examples -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @ekino/logger 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Travis CI][travis-image]][travis-url] 5 | [![Coverage Status][coverage-image]][coverage-url] 6 | [![styled with prettier][prettier-image]][prettier-url] 7 | 8 | A Lightweight logger that combines debug namespacing capabilities with winston levels and multioutput 9 | 10 | - [Installation](#installation) 11 | - [Usage](#usage) 12 | - [Using context ID](#using-context-id) 13 | - [Using namespaces](#using-namespaces) 14 | - [Using Logging Namespaces](#using-logging-namespaces) 15 | - [Outputs](#outputs) 16 | - [JSON](#json) 17 | - [Pretty](#pretty) 18 | - [Output function](#output-function) 19 | - [JSON Stringify utility](#json-stringify-utility) 20 | - [Log data](#log-data) 21 | - [Adding global metadata](#adding-global-metadata) 22 | - [TypeScript](#typescript) 23 | 24 | ## Installation 25 | 26 | Using npm: 27 | 28 | ```sh 29 | npm install @ekino/logger 30 | ``` 31 | 32 | Or yarn: 33 | 34 | ```sh 35 | yarn add @ekino/logger 36 | ``` 37 | 38 | ## Usage 39 | 40 | By default, the logger output warn and error levels for all namespaces. 41 | You can set LOG_LEVEL environment to override the default behavior. 42 | By default, it writes logs to stdout in JSON format 43 | 44 | The logger api allows you to set log level for all namespaces. For advanced usage, you define it even per namespace. 45 | 46 | A log instance is bounded to a namespace. To use it, instantiate a logger with a namespace and call a log function. 47 | 48 | This logger define 5 log levels: error, warn, info, debug, trace. 49 | When you set a level, all levels above it are enabled too. 50 | Log level can be set by calling `setLevel` function. 51 | 52 | For example, enabling `info` will enable `info`, `warn` and `error` but not `debug` or `trace`. 53 | The "special" log level `none` means no log and can only be used to set a namespace level. 54 | 55 | ```js 56 | { trace: 0, debug: 1, info: 2, warn: 3, error: 4 } 57 | ``` 58 | 59 | The basic log function signature is: 60 | 61 | ```js 62 | my_log.the_level(message, data) // With data an object holding informations usefull for debug purpose 63 | ``` 64 | 65 | Example 66 | 67 | ```js 68 | const { setNamespaces, setLevel, createLogger } = require('@ekino/logger') 69 | 70 | setNamespaces('root:*') 71 | setLevel('debug') 72 | 73 | const logger = createLogger('root:testing') 74 | logger.debug('sample message', { 75 | foo: 'bar', 76 | }) 77 | ``` 78 | 79 | output: 80 | 81 | ![Example](docs/images/example_usage1.gif) 82 | 83 | ### Using context ID 84 | 85 | One of the main complexity working with node is ability to follow all logs attached to one call or one function. 86 | This is not mandatory, but based on our experience, we recommend as a best practice to add a unique identifier that will be passed all along functions calls. 87 | When you log something, you can provide this id as a first parameter and logger will log it. If not provided, it's auto generated. 88 | 89 | The signature of the function with contextId is: 90 | 91 | ```js 92 | my_log.the_level(contextId, message, data) 93 | ``` 94 | 95 | Example app.js 96 | 97 | ```javascript 98 | const { setNamespaces, setLevel, createLogger } = require('@ekino/logger') 99 | 100 | setNamespaces('root:*') 101 | setLevel('debug') 102 | 103 | const logger = createLogger('root:testing') 104 | logger.debug('ctxId', 'log with predefined context ID', { 105 | foo: 'bar', 106 | }) 107 | ``` 108 | 109 | output: 110 | 111 | ![Example](docs/images/example_usage2.gif) 112 | 113 | ### Using namespaces 114 | 115 | Logger relies on namespaces. When you want to log something, you should define a namespace that is bound to it. 116 | When you debug, this gives you the flexibility to enable only the namespaces you need to output. 117 | As a good practice, we recommend setting a namespace by folder / file. 118 | For example for a file in modules/login/dao you could define 'modules:login:dao'. 119 | Warning, "=" can't be part of the namespace as it's a reserved symbol. 120 | 121 | You can also define a level per namespace. If no level is defined, the default global level is used. 122 | To disable logs of a namespace, you can specify a level `none` 123 | A namespace ':\*' means eveything after ':' will be enabled. Namespaces are parsed as regexp. 124 | 125 | To define namespace level, you should suffix namespace with "=the_level" 126 | For example let's say you need to enable all info logs but for debug purpose you need to lower the level 127 | of the namespace database to `debug`. You could then use: 128 | 129 | ```js 130 | const { setLevel, setNamespaces } = require('@ekino/logger') 131 | 132 | setLevel('info') 133 | setNamespaces('*,database*=debug,database:redis*=none') 134 | ``` 135 | 136 | #### Using Logging Namespaces 137 | 138 | ```js 139 | const { setNamespaces, setLevel, createLogger } = require('@ekino/logger') 140 | 141 | setNamespaces('namespace:*, namespace:mute=none') 142 | setLevel('debug') 143 | 144 | const loggerA = createLogger('namespace:subNamespace') 145 | const loggerB = createLogger('namespace:mute') 146 | 147 | loggerA.debug('Will be logged') 148 | loggerB.info('Will not be logged') 149 | ``` 150 | 151 | ```js 152 | const { setNamespaces, setLevel, createLogger } = require('@ekino/logger') 153 | 154 | setNamespaces('*, wrongNamespace=none') 155 | setLevel('debug') 156 | 157 | const loggerA = createLogger('namespace:subNamespace') 158 | const loggerB = createLogger('wrongNamespace') 159 | 160 | loggerA.debug('Will be logged') 161 | loggerB.info('Will not be logged') 162 | ``` 163 | 164 | ### Outputs 165 | 166 | Logger allow you to provide your own output adapter to customize how and where to write logs. 167 | It's bundle by default with `pretty` adapter and `json` that both write to stdout. 168 | By default, json adapter is enabled. 169 | You can use multiple adapters at the same time 170 | 171 | #### JSON 172 | 173 | ```js 174 | const { setNamespaces, setLevel, setOutput, outputs, createLogger } = require('@ekino/logger') 175 | 176 | setNamespaces('namespace:*') 177 | setLevel('debug') 178 | setOutput(outputs.json) 179 | 180 | const logger = createLogger('namespace:subNamespace') 181 | logger.debug('ctxId', 'Will be logged', { 182 | someData: 'someValue', 183 | someData2: 'someValue', 184 | }) 185 | ``` 186 | 187 | output: 188 | 189 | ![Example](docs/images/example_usage3.gif) 190 | 191 | #### Pretty 192 | 193 | Pretty will output a yaml like content. 194 | 195 | ```js 196 | const { setNamespaces, setLevel, setOutput, outputs, createLogger } = require('@ekino/logger') 197 | 198 | setNamespaces('namespace:*') 199 | setLevel('debug') 200 | setOutput(outputs.pretty) 201 | 202 | const logger = createLogger('namespace:subNamespace') 203 | logger.debug('ctxId', 'Will be logged', { 204 | someData: 'someValue', 205 | someData2: 'someValue', 206 | }) 207 | ``` 208 | 209 | output: 210 | 211 | ![Example](docs/images/example_pretty.gif) 212 | 213 | #### Output function 214 | 215 | An output, is a function that will receive log data and should transform and store it 216 | 217 | Log data follow the format: 218 | 219 | ``` 220 | { 221 | time: Date, 222 | level: string, 223 | namespace: string, 224 | contextId: string, 225 | meta: { any data defined in global context }, 226 | message: string, 227 | data: object 228 | } 229 | ``` 230 | 231 | ```js 232 | const { setNamespaces, setLevel, setOutput, outputs, outputUtils, createLogger } = require('@ekino/logger') 233 | 234 | setNamespaces('namespace:*') 235 | setLevel('debug') 236 | 237 | const consoleAdapter = (log) => { 238 | console.log(outputUtils.stringify(log)) 239 | } 240 | 241 | // This will output in stdout with the pretty output 242 | // and in the same will log through native console.log() function (usually to stdout too) 243 | setOutput([outputs.pretty, consoleAdapter]) 244 | 245 | const logger = createLogger('namespace:subNamespace') 246 | logger.debug('ctxId', 'Will be logged', { 247 | someData: 'someValue', 248 | someData2: 'someValue', 249 | }) 250 | ``` 251 | 252 | #### JSON Stringify utility 253 | 254 | To ease the creation of an output adapter, we provide a utility to stringify a json object that support circular reference 255 | and add stack to output for errors. 256 | 257 | ```js 258 | const { outputUtils } = require('@ekino/logger') 259 | 260 | const consoleAdapter = (log) => { 261 | console.log(outputUtils.stringify(log)) 262 | } 263 | ``` 264 | 265 | ### Log data 266 | 267 | Most of the time, a log message is not enough to guess context. 268 | You can append arbitrary data to your logs. 269 | If you're using some kind of log collector, you'll then be able to extract those values and inject them in elasticsearch for example. 270 | 271 | ```js 272 | const { setOutput, setNamespaces, setLevel, createLogger } = require('@ekino/logger') 273 | 274 | setOutput('pretty') 275 | setNamespaces('namespace:*') 276 | setLevel('info') 277 | 278 | const logger = createLogger('namespace:subNamespace') 279 | logger.warn('message', { someData: 'someValue' }) 280 | ``` 281 | 282 | output: 283 | 284 | ![Example](docs/images/example_data.gif) 285 | 286 | ### Force Log 287 | 288 | You can force to write the log even the logLevel isn't enabled. 289 | 290 | ```js 291 | const { setOutput, setNamespaces, setLevel, createLogger } = require('@ekino/logger') 292 | 293 | setOutput('pretty') 294 | setNamespaces('namespace:*') 295 | setLevel('info') 296 | 297 | const log = logger.createLogger('namespace', true) 298 | const num = 1 299 | log.debug('Will be logged', { someData: 'someValue' }, num > 0) 300 | ``` 301 | 302 | #### Adding global metadata 303 | 304 | Sometimes, you need to identify to which version or which application the logs refers to. 305 | To do so, we provide a function to set informations that will be added to the each log at a top level key. 306 | 307 | ```js 308 | const { setOutput, setNamespaces, setLevel, setGlobalContext, createLogger } = require('@ekino/logger') 309 | 310 | setOutput('pretty') 311 | setNamespaces('*') 312 | setLevel('info') 313 | setGlobalContext({ version: '2.0.0', env: 'dev' }) 314 | 315 | const logger = createLogger('namespace') 316 | logger.warn('message', { someData: 'someValue' }) 317 | ``` 318 | 319 | output: 320 | 321 | ![Example](docs/images/example_context.gif) 322 | 323 | ## TypeScript 324 | 325 | This package provides its own definition, so it can be easily used with TypeScript. 326 | 327 | [npm-image]: https://img.shields.io/npm/v/@ekino/logger.svg?style=flat-square 328 | [npm-url]: https://www.npmjs.com/package/@ekino/logger 329 | [travis-image]: https://img.shields.io/travis/ekino/node-logger.svg?style=flat-square 330 | [travis-url]: https://travis-ci.org/ekino/node-logger 331 | [prettier-image]: https://img.shields.io/badge/styled_with-prettier-ff69b4.svg?style=flat-square 332 | [prettier-url]: https://github.com/prettier/prettier 333 | [coverage-image]: https://img.shields.io/coveralls/ekino/node-logger/master.svg?style=flat-square 334 | [coverage-url]: https://coveralls.io/github/ekino/node-logger?branch=master 335 | -------------------------------------------------------------------------------- /docs/images/example_context.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekino/node-logger/962984800b12912447da49d8d7917872775827fe/docs/images/example_context.gif -------------------------------------------------------------------------------- /docs/images/example_context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekino/node-logger/962984800b12912447da49d8d7917872775827fe/docs/images/example_context.png -------------------------------------------------------------------------------- /docs/images/example_data.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekino/node-logger/962984800b12912447da49d8d7917872775827fe/docs/images/example_data.gif -------------------------------------------------------------------------------- /docs/images/example_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekino/node-logger/962984800b12912447da49d8d7917872775827fe/docs/images/example_data.png -------------------------------------------------------------------------------- /docs/images/example_pretty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekino/node-logger/962984800b12912447da49d8d7917872775827fe/docs/images/example_pretty.gif -------------------------------------------------------------------------------- /docs/images/example_pretty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekino/node-logger/962984800b12912447da49d8d7917872775827fe/docs/images/example_pretty.png -------------------------------------------------------------------------------- /docs/images/example_usage1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekino/node-logger/962984800b12912447da49d8d7917872775827fe/docs/images/example_usage1.gif -------------------------------------------------------------------------------- /docs/images/example_usage1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekino/node-logger/962984800b12912447da49d8d7917872775827fe/docs/images/example_usage1.png -------------------------------------------------------------------------------- /docs/images/example_usage2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekino/node-logger/962984800b12912447da49d8d7917872775827fe/docs/images/example_usage2.gif -------------------------------------------------------------------------------- /docs/images/example_usage2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekino/node-logger/962984800b12912447da49d8d7917872775827fe/docs/images/example_usage2.png -------------------------------------------------------------------------------- /docs/images/example_usage3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekino/node-logger/962984800b12912447da49d8d7917872775827fe/docs/images/example_usage3.gif -------------------------------------------------------------------------------- /docs/images/example_usage3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekino/node-logger/962984800b12912447da49d8d7917872775827fe/docs/images/example_usage3.png -------------------------------------------------------------------------------- /examples/example1.js: -------------------------------------------------------------------------------- 1 | const logger = require('../lib/index') 2 | 3 | logger.setNamespaces('root:*') 4 | logger.setLevel('debug') 5 | 6 | const log = logger.createLogger('root:testing') 7 | log.debug('sample message', { 8 | foo: 'bar', 9 | }) -------------------------------------------------------------------------------- /examples/example2.js: -------------------------------------------------------------------------------- 1 | const logger = require('../lib/index') 2 | 3 | logger.setNamespaces('root:*') 4 | logger.setLevel('debug') 5 | 6 | const log = logger.createLogger('root:testing') 7 | log.debug('ctxId', 'log with predefined context ID', { 8 | foo: 'bar', 9 | }) -------------------------------------------------------------------------------- /examples/example3.js: -------------------------------------------------------------------------------- 1 | const logger = require('../lib/index') 2 | 3 | logger.setNamespaces('namespace:*') 4 | logger.setLevel('debug') 5 | //logger.setOutput(logger.outputs.json) 6 | 7 | const log = logger.createLogger('namespace:subNamespace') 8 | log.debug('ctxId', 'Will be logged', { someData: 'someValue', someData2: 'someValue' }) -------------------------------------------------------------------------------- /examples/example_context.js: -------------------------------------------------------------------------------- 1 | const logger = require('../lib/index') 2 | 3 | logger.setOutput(logger.outputs.pretty) 4 | logger.setNamespaces('*') 5 | logger.setLevel('info') 6 | logger.setGlobalContext({ version: '2.0.0', env: 'dev' }) 7 | 8 | const log = logger.createLogger('namespace') 9 | 10 | log.warn('message', { someData: 'someValue' }) -------------------------------------------------------------------------------- /examples/example_data.js: -------------------------------------------------------------------------------- 1 | const logger = require('../lib/index') 2 | 3 | logger.setOutput(logger.outputs.pretty) 4 | logger.setNamespaces('namespace:*') 5 | logger.setLevel('info') 6 | 7 | const log = logger.createLogger('namespace:subNamespace') 8 | 9 | log.warn('message', { someData: 'someValue' }) 10 | -------------------------------------------------------------------------------- /examples/example_force.js: -------------------------------------------------------------------------------- 1 | const logger = require('../lib/index') 2 | 3 | logger.setOutput(logger.outputs.pretty) 4 | logger.setNamespaces('*') 5 | logger.setLevel('info') 6 | 7 | const log = logger.createLogger('namespace', true) 8 | const num = 1 9 | log.debug('Will be logged', { someData: 'someValue' }, num > 0) 10 | -------------------------------------------------------------------------------- /examples/example_pretty.js: -------------------------------------------------------------------------------- 1 | const logger = require('../lib/index') 2 | 3 | logger.setNamespaces('namespace:*') 4 | logger.setLevel('debug') 5 | logger.setOutput(logger.outputs.pretty) 6 | 7 | const log = logger.createLogger('namespace:subNamespace') 8 | log.debug('ctxId', 'Will be logged', { someData: 'someValue', someData2: 'someValue' }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ekino/logger", 3 | "description": "A Lightweight logger that combines debug namespacing capabilities with winston levels and multioutput", 4 | "homepage": "https://github.com/ekino/node-logger", 5 | "license": "MIT", 6 | "version": "2.1.1", 7 | "main": "lib/index.js", 8 | "types": "lib/index.d.ts", 9 | "files": [ 10 | "/lib" 11 | ], 12 | "tags": [ 13 | "logger", 14 | "lightweight", 15 | "namespaces" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/ekino/node-logger.git" 20 | }, 21 | "maintainers": [ 22 | { 23 | "name": "Ekino" 24 | } 25 | ], 26 | "engines": { 27 | "node": ">=14" 28 | }, 29 | "dependencies": { 30 | "colors": "1.x", 31 | "prettyoutput": "1.x", 32 | "uuid": "8.x" 33 | }, 34 | "devDependencies": { 35 | "@commitlint/config-conventional": "17.x", 36 | "@types/node": "18.x", 37 | "@types/uuid": "8.x", 38 | "ava": "4.x", 39 | "commitlint": "17.x", 40 | "conventional-changelog": "3.x", 41 | "conventional-changelog-cli": "2.x", 42 | "coveralls": "3.x", 43 | "eslint": "8.x", 44 | "husky": "8.x", 45 | "nyc": "15.x", 46 | "prettier": "2.x", 47 | "sinon": "14.x", 48 | "ts-node": "10.x", 49 | "typescript": "4.x" 50 | }, 51 | "scripts": { 52 | "fmt": "prettier --color --write \"{*,test/**/*}.{js,ts}\" --cache", 53 | "check-fmt": "prettier --list-different \"{*,test/**/*}.{js,ts}\"", 54 | "build": "tsc --build tsconfig.lib.json", 55 | "prepublishOnly": "rm -rf lib && yarn build", 56 | "test": "ava", 57 | "test-cover": "nyc ava", 58 | "coverage": "nyc ava | coveralls", 59 | "version": "echo ${npm_package_version}", 60 | "lint": "eslint ." 61 | }, 62 | "eslintIgnore": [ 63 | "lib", 64 | "examples" 65 | ], 66 | "ava": { 67 | "files": [ 68 | "test/**/*.js" 69 | ], 70 | "extensions": [ 71 | "js" 72 | ], 73 | "require": [ 74 | "ts-node/register" 75 | ] 76 | }, 77 | "nyc": { 78 | "reporter": [ 79 | "lcov", 80 | "text-lcov" 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/definitions.ts: -------------------------------------------------------------------------------- 1 | export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'none' 2 | 3 | export type Log = { 4 | level: LogLevel 5 | time: Date 6 | namespace: string 7 | contextId: string 8 | meta: Record 9 | message: string 10 | data?: Record 11 | } 12 | export type Output = Pick 13 | 14 | export type NameSpaceConfig = { 15 | regex?: RegExp 16 | level?: number 17 | } 18 | 19 | export interface Logger { 20 | trace: LogMethod 21 | debug: LogMethod 22 | info: LogMethod 23 | warn: LogMethod 24 | error: LogMethod 25 | isLevelEnabled(level: string): boolean | undefined 26 | canForceWrite?: boolean 27 | } 28 | 29 | export interface OutputAdapter { 30 | (log: Log): void 31 | } 32 | 33 | export interface LogMethod { 34 | (contextId: string, message: string, data?: unknown, forceLogging?: boolean): void 35 | (message: string, data?: unknown, forceLogging?: boolean): void 36 | } 37 | 38 | export interface Internal { 39 | loggers: Record 40 | namespaces: NameSpaceConfig[] 41 | levels: LogLevel[] 42 | level?: number 43 | outputs: OutputAdapter[] 44 | globalContext: Record 45 | isEnabled?(namespace: string, index: number): boolean 46 | } 47 | 48 | export type LogColor = 'red' | 'yellow' | 'blue' | 'white' | 'grey' 49 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | import { Internal, Logger, LogLevel, Log, NameSpaceConfig, OutputAdapter, LogMethod } from './definitions' 3 | 4 | import * as outputs from './output_adapters' 5 | import * as outputUtils from './output_utils' 6 | import { isObject } from './output_utils' 7 | 8 | /** 9 | * @typedef {Function} LoggerLogFunction 10 | * @param {String} [contextId] - a identifier used to group log associated to a seeam task / request 11 | * @param {String} message - A description 12 | * @param {Object} [data] - Anything useful to understand the error 13 | */ 14 | 15 | /** 16 | * @typedef {Function} LoggerIsEnabledFunction 17 | * @param {String} level 18 | * @returns {Boolean} true if level is enabled 19 | */ 20 | 21 | /** 22 | * @typedef {Object} Logger 23 | * @property {LoggerLogFunction} trace - Log to trace level 24 | * @property {LoggerLogFunction} debug - Log to debug level 25 | * @property {LoggerLogFunction} info - Log to info level 26 | * @property {LoggerLogFunction} warn - Log to warning level 27 | * @property {LoggerLogFunction} error - Log to error level 28 | * @property {LoggerIsEnabledFunction} isLoggerEnabled - check if logger is enabled for a level 29 | */ 30 | 31 | /** 32 | * @typedef {object} Log 33 | * @property {string} level - log level (debug, info, warn, error) 34 | * @property {Date} time - log time 35 | * @property {string} namespace - log namespace 36 | * @property {string} contextId - contextId 37 | * @property {object} meta - Some meta and additional data from globalContext 38 | * @property {string} message - log message 39 | * @property {object} [data] - Additional data to understand log message 40 | */ 41 | 42 | /** 43 | * @typedef {Function} OutputAdapter 44 | * @param {Log} log to write 45 | */ 46 | 47 | /** 48 | * @typedef {Object} NamespaceConfig 49 | * @property {number} [level] 50 | * @property {RegExp|null} regex 51 | */ 52 | 53 | /************* INTERNALS *************/ 54 | export const internals: Internal = { 55 | loggers: {}, 56 | levels: ['trace', 'debug', 'info', 'warn', 'error', 'none'], 57 | outputs: [outputs.json], 58 | level: undefined, 59 | namespaces: [], 60 | globalContext: {}, 61 | } 62 | 63 | /** 64 | * True if both namespace and level are enabled. 65 | * @param {String} namespace 66 | * @param {String} level 67 | * @return {Boolean} true if enabled 68 | */ 69 | internals.isEnabled = (namespace, level): boolean => { 70 | let nsLevel = internals.level || 0 71 | let nsMatch = false 72 | const internalNamespaces = internals.namespaces 73 | internalNamespaces 74 | .slice() 75 | .reverse() 76 | .forEach((ns) => { 77 | if (ns.regex?.test(namespace)) { 78 | nsMatch = true 79 | if (ns.level) { 80 | nsLevel = ns.level 81 | return false 82 | } 83 | } 84 | }) 85 | 86 | return nsMatch && level >= nsLevel 87 | } 88 | 89 | /************* EXPORTS *************/ 90 | /** 91 | * @typedef {Function} createLogger 92 | * @param {String} [namespace] 93 | * @param {boolean} canForceWrite 94 | * @return {Logger} 95 | */ 96 | export const createLogger = (namespace?: string, canForceWrite?: boolean): Logger => { 97 | namespace = namespace || '' 98 | 99 | let logger = internals.loggers?.[namespace] 100 | if (logger) return logger 101 | 102 | logger = syncLogger({} as Logger, namespace, canForceWrite) 103 | if (internals.loggers) internals.loggers[namespace] = logger 104 | 105 | return logger 106 | } 107 | 108 | /** 109 | * Define enabled / disabled namespaces 110 | * @param {string} namespace 111 | */ 112 | export const setNamespaces = (namespace: string): void => { 113 | internals.namespaces = [] 114 | 115 | if (!namespace) return syncLoggers() 116 | 117 | const splitNamespaces = namespace.replace(/\s/g, '').split(',') 118 | 119 | splitNamespaces.forEach((name) => { 120 | const parsedNamespace = parseNamespace(name) 121 | if (!parsedNamespace) return 122 | 123 | internals.namespaces.push(parsedNamespace) 124 | }) 125 | 126 | syncLoggers() 127 | } 128 | 129 | /** 130 | * Change log level 131 | * @param {string} level - one of trace, debug, info, warn, error 132 | */ 133 | export const setLevel = (level: LogLevel): void => { 134 | if (!internals.levels?.includes(level)) { 135 | throw new Error(`Invalid level: '${level}'`) 136 | } 137 | 138 | // internally store corresponding level index 139 | internals.level = internals.levels?.indexOf(level) 140 | 141 | syncLoggers() 142 | } 143 | 144 | /** 145 | * Set outputs transport to use 146 | * @param {Array|OutputAdapter} outputAdapters 147 | */ 148 | export const setOutput = (outputAdapters?: OutputAdapter[] | OutputAdapter): void => { 149 | if (!outputAdapters) outputAdapters = [] 150 | if (!Array.isArray(outputAdapters)) outputAdapters = [outputAdapters] 151 | 152 | outputAdapters.forEach((output) => { 153 | if (typeof output !== 'function') throw new Error(`Invalid output: '${output}'`) 154 | }) 155 | 156 | internals.outputs = outputAdapters 157 | } 158 | 159 | /** 160 | * Set a global context to append to all logs, 161 | * useful to append application/service name globally for example. 162 | * Be warned this context will be added to all logs, 163 | * even those from third party libraries if they use this module. 164 | * @param {Object} context - The object holding default context data 165 | */ 166 | export const setGlobalContext = (context: Record): void => { 167 | internals.globalContext = context 168 | } 169 | 170 | /** 171 | * @type {function} 172 | * Return an id that can be used as a contextId 173 | * @return {string} 174 | */ 175 | export const id = (): string => { 176 | return uuidv4() 177 | } 178 | 179 | /** 180 | * Parse a namespace to extract level, namespace (eg: ns1:subns1=info) 181 | * @param {string} namespace 182 | * @return {NamespaceConfig|null} 183 | */ 184 | export const parseNamespace = (namespace: string): NameSpaceConfig | null => { 185 | const matches = /([^=]*)(=(.*))?/.exec(namespace) 186 | if (!matches) return null 187 | 188 | let level 189 | if (matches[3]) { 190 | const idx = internals.levels?.findIndex((l) => l === matches[3]) 191 | 192 | if (idx === undefined || idx < 0) throw new Error(`Level ${matches[3]} is not a valid log level : ${internals.levels}`) 193 | level = idx 194 | } 195 | 196 | let pattern = matches[1] 197 | if (!pattern) return null 198 | 199 | pattern = pattern.replace(/\*/g, '.*?') 200 | const regex = new RegExp(`^${pattern}$`) 201 | 202 | const namespaceConfig: NameSpaceConfig = { regex } 203 | if (level) namespaceConfig.level = level 204 | 205 | return namespaceConfig 206 | } 207 | 208 | /** 209 | * Log method. Write to stdout as a JSON object 210 | * @param {String} namespace 211 | * @param {String} level 212 | * @param {String} [contextId] 213 | * @param {String} message 214 | * @param {Object} [data] - An object holding data to help understand the error 215 | * @param {boolean} forceLogging 216 | */ 217 | export const log = ( 218 | namespace: string, 219 | level: LogLevel, 220 | contextId?: string | null, 221 | message?: string | Record | null, 222 | data?: Record, 223 | forceLogging?: boolean | Record 224 | ): void => { 225 | if (isObject(message)) { 226 | forceLogging = data 227 | data = message 228 | message = contextId 229 | contextId = null 230 | } 231 | 232 | contextId = contextId || id() 233 | const time = new Date() 234 | const logInstance: Log = { level, time, namespace, contextId, meta: {}, message: message || contextId, data } 235 | if (internals.globalContext) logInstance.meta = Object.assign({}, internals.globalContext) 236 | 237 | if(forceLogging || internals.loggers[namespace]?.isLevelEnabled(level)) write(logInstance) 238 | } 239 | 240 | /** 241 | * Write log using output adapter 242 | * @param {Log} logInstance 243 | */ 244 | export const write = (logInstance: Log): void => { 245 | internals.outputs?.forEach((outputFn) => { 246 | outputFn(logInstance) 247 | }) 248 | } 249 | 250 | /** 251 | * Remove all properties but levels. 252 | * Levels contains a function that does nothing if namespace or level is disable. 253 | * If enabled, calls log function. 254 | * @param {Logger} logger 255 | * @param {String} namespace 256 | * @param {boolean} canForceWrite 257 | * @return {Logger} 258 | */ 259 | export const syncLogger = (logger: Logger, namespace: string, canForceWrite?: boolean): Logger => { 260 | for (const key in logger) { 261 | delete logger[key as keyof Logger] 262 | } 263 | 264 | const enabledLevels: Record = {} 265 | if (internals.levels) { 266 | internals.levels.forEach((level, idx) => { 267 | if (level === 'none') return 268 | const levelIsEnabled = internals.isEnabled?.(namespace, idx) ?? false 269 | if ( levelIsEnabled || canForceWrite) { 270 | enabledLevels[level] = levelIsEnabled 271 | 272 | logger[level] = ((contextId: string, message: string, data?: Record, forceLogging?: boolean) => { 273 | log(namespace, level, contextId, message, data, forceLogging) 274 | }) as LogMethod 275 | } else { 276 | enabledLevels[level] = false 277 | logger[level] = () => {} 278 | } 279 | }) 280 | 281 | logger.isLevelEnabled = (level) => enabledLevels[level] 282 | } 283 | logger.canForceWrite = canForceWrite 284 | return logger 285 | } 286 | 287 | /** 288 | * ReSync all loggers level functions to enable / disable them 289 | * This should be called when namespaces or levels are updated 290 | */ 291 | export const syncLoggers = () => { 292 | for (const [namespace, logger] of Object.entries(internals.loggers)) { 293 | syncLogger(logger, namespace, logger.canForceWrite) 294 | } 295 | } 296 | 297 | /************* INIT *************/ 298 | const namespaces = process.env.LOGS || '*' 299 | const logLevel: LogLevel = (process.env.LOG_LEVEL as LogLevel) || 'warn' 300 | 301 | setNamespaces(namespaces) 302 | setLevel(logLevel) 303 | 304 | /************* EXPORT *************/ 305 | export * from './definitions' 306 | export { outputUtils, outputs } 307 | export default { 308 | createLogger, 309 | setLevel, 310 | setNamespaces, 311 | setOutput, 312 | setGlobalContext, 313 | id, 314 | outputUtils, 315 | outputs 316 | } 317 | -------------------------------------------------------------------------------- /src/output_adapters.ts: -------------------------------------------------------------------------------- 1 | import colors from 'colors/safe' 2 | import * as outputUtils from './output_utils' 3 | import { LogColor, Log, LogLevel, Output } from './definitions' 4 | const prettyOutput = require('prettyoutput') 5 | 6 | /** 7 | * Object mapping log color and log level 8 | * @param {Record} levelColorMap 9 | */ 10 | const levelColorMap: Record = { 11 | none: 'red', 12 | error: 'red', 13 | warn: 'yellow', 14 | info: 'blue', 15 | debug: 'white', 16 | trace: 'grey', 17 | } 18 | 19 | /** 20 | * Make sure that we get a 2 digit number by beginning with a 0 if length < 2 21 | * @param {number|string} num 22 | * @returns {string} 23 | */ 24 | export const twoDigitNumber = (num?: number | string): string => { 25 | return num != null ? (`${num}`.length < 2 ? `0${num}` : `${num}`) : '' 26 | } 27 | 28 | /** 29 | * Format time as "yyyy-mm-dd hh:mm:ss" 30 | * @param {Date} time 31 | * @returns {string} 32 | */ 33 | export const prettyTime = (time?: Date): string | undefined => { 34 | if (!time) return undefined 35 | 36 | const year = twoDigitNumber(time.getFullYear()) 37 | const month = twoDigitNumber(time.getMonth() + 1) 38 | const day = twoDigitNumber(time.getDate()) 39 | const hours = twoDigitNumber(time.getUTCHours()) 40 | const minutes = twoDigitNumber(time.getMinutes()) 41 | const seconds = twoDigitNumber(time.getSeconds()) 42 | 43 | return `${year}-${twoDigitNumber(month)}-${day} ${hours}:${minutes}:${seconds}` 44 | } 45 | 46 | /** 47 | * Log with pretty output formater in stdout 48 | * @param {Log} log 49 | */ 50 | export const pretty = (log: Log): void => { 51 | const time = prettyTime(log.time) 52 | const defaultLevel = log.level || 'error' 53 | 54 | const levelColor = levelColorMap[defaultLevel] 55 | const infos = `${time} (${log.namespace}) [${defaultLevel}] : ` 56 | const output: Output = { contextId: log.contextId, meta: log.meta, data: log.data } 57 | 58 | const result = `${infos}${colors[levelColor](log.message || '')}\n${prettyOutput(output, { maxDepth: 6 }, 2)}` 59 | 60 | process.stdout.write(result) 61 | process.stdout.write('\n') 62 | } 63 | 64 | /** 65 | * Log in json to stdout 66 | * @param {Log} log 67 | */ 68 | export const json = (log: Log): void => { 69 | const output = Object.assign({ 70 | level: log.level, 71 | time: log.time?.toISOString(), 72 | namespace: log.namespace, 73 | contextId: log.contextId, 74 | ...log.meta, 75 | message: log.message, 76 | data: log.data, 77 | }) 78 | 79 | const result = outputUtils.stringify(output) 80 | 81 | process.stdout.write(result) 82 | process.stdout.write('\n') 83 | } 84 | -------------------------------------------------------------------------------- /src/output_utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Replace circular reference when used with JSON.stringify 3 | * Usage : JSON.stringify(element, getCircularReplacer()) 4 | */ 5 | 6 | export const getCircularReplacer = (): any => { 7 | const seen = new WeakSet() 8 | return (key: string | number, value: any) => { 9 | if (typeof value === 'object' && value !== null) { 10 | if (seen.has(value)) { 11 | return 12 | } 13 | seen.add(value) 14 | } 15 | return value 16 | } 17 | } 18 | 19 | /** 20 | * JSON.stringify with support for errors descriptions 21 | * You should add a try catch around it to avoid error 22 | * @param {*} log - json object 23 | * @returns {string} - stringified log or error log if can not stringify 24 | */ 25 | export const stringify = (log: Record): string => { 26 | try { 27 | return JSON.stringify(log) 28 | } catch (e) { 29 | return JSON.stringify(log, getCircularReplacer()) 30 | } 31 | } 32 | 33 | /** 34 | * Used to override error toJSON function to customize output 35 | * @return {object} 36 | */ 37 | export const errorToJson = (obj: any): Record => { 38 | const result: Record = {} 39 | 40 | Object.getOwnPropertyNames(obj).forEach(function (key) { 41 | result[key] = obj[key] 42 | }, obj) 43 | 44 | return result 45 | } 46 | 47 | export const isObject = (val: unknown): val is Record => !!val && typeof val === 'object' && !Array.isArray(val) 48 | -------------------------------------------------------------------------------- /test/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-disable no-shadow */ 3 | 4 | const test = require('ava') 5 | const sinon = require('sinon') 6 | 7 | const logger = require('../src/index.ts') 8 | 9 | test.beforeEach((t) => { 10 | logger.setOutput([]) 11 | }) 12 | 13 | test('A logger instance should have levels function and isLevelEnabled function', (t) => { 14 | const log = logger.createLogger() 15 | t.is(typeof log.trace, 'function') 16 | t.is(typeof log.debug, 'function') 17 | t.is(typeof log.info, 'function') 18 | t.is(typeof log.warn, 'function') 19 | t.is(typeof log.error, 'function') 20 | t.is(typeof log.isLevelEnabled, 'function') 21 | t.is(typeof log.none, 'undefined') 22 | }) 23 | 24 | test('A logger instance should only accept functions', (t) => { 25 | const error = t.throws( 26 | () => { 27 | logger.setOutput('invalid') 28 | }, 29 | { instanceOf: Error } 30 | ) 31 | 32 | t.is(error.message, `Invalid output: 'invalid'`) 33 | }) 34 | 35 | test('A logger instance should only accept allowed levels', (t) => { 36 | const error = t.throws( 37 | () => { 38 | logger.setLevel('invalid') 39 | }, 40 | { instanceOf: Error } 41 | ) 42 | 43 | t.is(error.message, `Invalid level: 'invalid'`) 44 | }) 45 | 46 | test('A logger instance should log if level and namespace are enabled', (t) => { 47 | logger.setNamespaces('*') 48 | logger.setLevel('info') 49 | 50 | const log = logger.createLogger() 51 | const spy = sinon.spy(logger, 'write') 52 | 53 | log.info(null) 54 | t.true(spy.calledOnce) 55 | 56 | logger.write.restore() 57 | }) 58 | 59 | test("A logger instance shouldn't log if level is lower than enabled level", (t) => { 60 | logger.setNamespaces('*') 61 | logger.setLevel('info') 62 | 63 | const log = logger.createLogger() 64 | const spy = sinon.spy(logger, 'write') 65 | 66 | log.debug(null, 'test') 67 | 68 | t.is(spy.callCount, 0) 69 | 70 | logger.write.restore() 71 | }) 72 | 73 | test('A logger instance should log if instance can forceWrite and forceLogging is truthy', (t) => { 74 | logger.setNamespaces('*') 75 | logger.setLevel('info') 76 | 77 | const log = logger.createLogger('test', true) 78 | const spy = sinon.spy(logger, 'write') 79 | 80 | log.debug(null, {}, true) 81 | 82 | t.is(spy.callCount, 1) 83 | 84 | logger.write.restore() 85 | }) 86 | 87 | test('setLevel and setNamespace should not reset canForceWrite', (t) => { 88 | const log = logger.createLogger('test', true) 89 | logger.setNamespaces('*') 90 | logger.setLevel('info') 91 | const spy = sinon.spy(logger, 'write') 92 | 93 | log.debug(null, {}, true) 94 | 95 | t.is(spy.callCount, 1) 96 | 97 | logger.write.restore() 98 | }) 99 | 100 | test("A logger instance shouldn't log if namespace is not enabled", (t) => { 101 | logger.setNamespaces('test:*') 102 | 103 | logger.setLevel('info') 104 | 105 | const log = logger.createLogger('default') 106 | const spy = sinon.spy(logger, 'write') 107 | 108 | log.info(null, 'test') 109 | 110 | t.is(spy.callCount, 0) 111 | 112 | logger.write.restore() 113 | }) 114 | 115 | test("A logger instance shouldn't log if log level is lower than namespace pattern level", (t) => { 116 | logger.setNamespaces('test:*=error') 117 | 118 | logger.setLevel('info') 119 | 120 | const log = logger.createLogger('test:subtest') 121 | const spy = sinon.spy(logger, 'write') 122 | 123 | log.info(null, 'test') 124 | 125 | t.is(spy.callCount, 0) 126 | 127 | logger.write.restore() 128 | }) 129 | 130 | test('A logger instance should log if log level is higher or equal than namespace pattern level', (t) => { 131 | logger.setNamespaces('test:*=debug') 132 | 133 | logger.setLevel('info') 134 | 135 | const log = logger.createLogger('test:subtest') 136 | const spy = sinon.spy(logger, 'write') 137 | 138 | log.debug(null) 139 | t.true(spy.calledOnce) 140 | 141 | logger.write.restore() 142 | }) 143 | 144 | test('A logger instance should log according to state defined in the latest matching namespace in the list', (t) => { 145 | logger.setNamespaces('test:*=warn,test2:*,test:*=error,test2:*=none') 146 | 147 | logger.setLevel('info') 148 | 149 | const log = logger.createLogger('test:subtest') 150 | const log2 = logger.createLogger('test2:subtest') 151 | const spy = sinon.spy(logger, 'write') 152 | 153 | log.warn(null) 154 | log2.info('test') 155 | t.is(spy.callCount, 1) 156 | 157 | logger.write.restore() 158 | }) 159 | 160 | test('A logger should call an output adapter with log data, metadata, message and data', (t) => { 161 | logger.setNamespaces('test:*') 162 | logger.setLevel('info') 163 | 164 | const outputAdapter = sinon.spy() 165 | logger.setOutput(outputAdapter) 166 | 167 | const now = new Date() 168 | 169 | const log = logger.createLogger('test:subTest') 170 | const timersStub = sinon.useFakeTimers(now.getTime()) 171 | 172 | log.warn('ctxId', 'test', { someData: 'someValue' }) 173 | 174 | t.true(outputAdapter.calledOnce) 175 | 176 | const outputArg = outputAdapter.firstCall.args[0] 177 | 178 | t.is(outputArg.namespace, 'test:subTest') 179 | t.is(outputArg.level, 'warn') 180 | t.is(outputArg.time.getTime(), now.getTime()) 181 | t.is(outputArg.contextId, 'ctxId') 182 | t.is(outputArg.message, 'test') 183 | t.deepEqual(outputArg.data, { someData: 'someValue' }) 184 | 185 | timersStub.restore() 186 | }) 187 | 188 | test('A logger should call all output output adapters added', (t) => { 189 | logger.setNamespaces('test:*') 190 | logger.setLevel('info') 191 | 192 | const outputAdapter1 = sinon.spy() 193 | const outputAdapter2 = sinon.spy() 194 | logger.setOutput([outputAdapter1, outputAdapter2]) 195 | 196 | const now = new Date() 197 | 198 | const log = logger.createLogger('test:subTest') 199 | const timersStub = sinon.useFakeTimers(now.getTime()) 200 | 201 | log.warn('ctxId', 'test', { someData: 'someValue' }) 202 | 203 | t.true(outputAdapter1.calledOnce) 204 | t.true(outputAdapter2.calledOnce) 205 | 206 | const outputArg1 = outputAdapter1.firstCall.args[0] 207 | const outputArg2 = outputAdapter2.firstCall.args[0] 208 | 209 | t.is(outputArg1.namespace, 'test:subTest') 210 | t.is(outputArg1.level, 'warn') 211 | t.is(outputArg1.time.getTime(), now.getTime()) 212 | t.is(outputArg1.contextId, 'ctxId') 213 | t.is(outputArg1.message, 'test') 214 | t.deepEqual(outputArg1.data, { someData: 'someValue' }) 215 | 216 | t.is(outputArg2.namespace, 'test:subTest') 217 | t.is(outputArg2.level, 'warn') 218 | t.is(outputArg2.time.getTime(), now.getTime()) 219 | t.is(outputArg2.contextId, 'ctxId') 220 | t.is(outputArg2.message, 'test') 221 | t.deepEqual(outputArg2.data, { someData: 'someValue' }) 222 | 223 | timersStub.restore() 224 | }) 225 | 226 | test("A logger shoudn't throw an error if not outputs defined", (t) => { 227 | logger.setNamespaces('test:*') 228 | logger.setLevel('info') 229 | 230 | logger.setOutput() 231 | 232 | const log = logger.createLogger('test:subTest') 233 | 234 | log.warn('ctxId', 'test', { someData: 'someValue' }) 235 | t.true(true) 236 | }) 237 | 238 | test('A logger should support defining a global context', (t) => { 239 | logger.setNamespaces('test:*') 240 | logger.setLevel('info') 241 | logger.setGlobalContext({ service: 'logger', mode: 'testing' }) 242 | 243 | const outputAdapter = sinon.spy() 244 | logger.setOutput(outputAdapter) 245 | 246 | const now = new Date() 247 | 248 | const log = logger.createLogger('test:global:context') 249 | const timersStub = sinon.useFakeTimers(now.getTime()) 250 | 251 | log.warn('ctxId', 'test') 252 | 253 | t.true(outputAdapter.calledOnce) 254 | 255 | const outputArg = outputAdapter.firstCall.args[0] 256 | 257 | t.is(outputArg.namespace, 'test:global:context') 258 | t.is(outputArg.level, 'warn') 259 | t.is(outputArg.time.getTime(), now.getTime()) 260 | t.is(outputArg.contextId, 'ctxId') 261 | t.is(outputArg.meta.service, 'logger') 262 | t.is(outputArg.meta.mode, 'testing') 263 | t.is(outputArg.message, 'test') 264 | 265 | timersStub.restore() 266 | }) 267 | 268 | test('A logger contextId arg should be an an optional argument', (t) => { 269 | logger.setNamespaces('ns1:*') 270 | logger.setLevel('info') 271 | 272 | const outputAdapter = sinon.spy() 273 | logger.setOutput(outputAdapter) 274 | 275 | const now = new Date() 276 | 277 | const log = logger.createLogger('ns1:subns1') 278 | const timersStub = sinon.useFakeTimers(now.getTime()) 279 | 280 | log.warn('msg1', { key1: 'value1' }) 281 | 282 | t.true(outputAdapter.calledOnce) 283 | 284 | const outputArg = outputAdapter.firstCall.args[0] 285 | 286 | t.is(outputArg.level, 'warn') 287 | t.is(outputArg.time.getTime(), now.getTime()) 288 | t.is(typeof outputArg.contextId, 'string') 289 | t.is(outputArg.message, 'msg1') 290 | t.deepEqual(outputArg.data, { key1: 'value1' }) 291 | 292 | timersStub.restore() 293 | }) 294 | 295 | test("A logger should not log if it's namespace is disabled after call to setNamespaces", (t) => { 296 | logger.setNamespaces('*') 297 | logger.setLevel('info') 298 | 299 | const log = logger.createLogger('ns1') 300 | const spy = sinon.spy(logger, 'write') 301 | 302 | log.info(null, 'msg1') 303 | logger.setNamespaces('ns2:*,ns3:*') 304 | log.info(null, 'msg2') 305 | 306 | t.true(spy.calledOnce) 307 | t.is(spy.args[0][0].message, 'msg1') 308 | 309 | logger.write.restore() 310 | }) 311 | 312 | test('A logger should not log if log level is not upper after call to setLevel', (t) => { 313 | logger.setNamespaces('*') 314 | logger.setLevel('info') 315 | 316 | const log = logger.createLogger('ns1') 317 | const spy = sinon.spy(logger, 'write') 318 | 319 | log.info(null, 'msg1') 320 | logger.setLevel('warn') 321 | log.info(null, 'msg2') 322 | 323 | t.true(spy.calledOnce) 324 | t.is(spy.args[0][0].message, 'msg1') 325 | 326 | logger.write.restore() 327 | }) 328 | 329 | test('A logger should not log if upper namespace was enabled, but sub namespace level was set to none', (t) => { 330 | logger.setNamespaces('ns1:*,ns1:subns1=none') 331 | logger.setLevel('info') 332 | 333 | const log = logger.createLogger('ns1:subns1') 334 | const spy = sinon.spy(logger, 'write') 335 | 336 | log.info(null, 'msg1') 337 | 338 | t.is(spy.callCount, 0) 339 | 340 | logger.write.restore() 341 | }) 342 | 343 | test('A logger should return true for a call to isLevelEnabled if level and namespace is enabled', (t) => { 344 | logger.setNamespaces('ns1:*,ns1:subns1=none') 345 | logger.setLevel('info') 346 | 347 | const log = logger.createLogger('ns1:subns2') 348 | t.true(log.isLevelEnabled('warn')) 349 | }) 350 | 351 | test('A logger should return false for a call to isLevelEnabled if namespace level was set to none', (t) => { 352 | logger.setNamespaces('ns1:*,ns1:subns1=none') 353 | logger.setLevel('info') 354 | 355 | const log = logger.createLogger('ns1:subns1') 356 | t.false(log.isLevelEnabled('warn')) 357 | }) 358 | 359 | test('A logger should return true for a call to isLevelEnabled if top namespace is enabled but another subnamespace is set to none', (t) => { 360 | logger.setNamespaces('ns1:*,ns1:subns1=none') 361 | logger.setLevel('error') 362 | 363 | const log = logger.createLogger('ns1:subns2') 364 | t.false(log.isLevelEnabled('warn')) 365 | }) 366 | 367 | test('loggers should be equal if they are for the same namespace', (t) => { 368 | logger.setNamespaces('ns1:*,ns1:subns1=none') 369 | logger.setLevel('error') 370 | 371 | const log1 = logger.createLogger('ns1:subns2') 372 | const log2 = logger.createLogger('ns1:subns2') 373 | t.is(log1, log2) 374 | }) 375 | 376 | test('parseNamespace should return a namespace if there is no level', (t) => { 377 | const result = logger.parseNamespace('test:*') 378 | t.deepEqual(result, { regex: /^test:.*?$/ }) 379 | }) 380 | 381 | test('parseNamespace should return a namespace and a level', (t) => { 382 | const result = logger.parseNamespace('test:*=info') 383 | t.deepEqual(result, { regex: /^test:.*?$/, level: 2 }) 384 | }) 385 | 386 | test('parseNamespace should return null if namespace is missing', (t) => { 387 | const result = logger.parseNamespace('=info') 388 | t.deepEqual(result, null) 389 | }) 390 | 391 | test('parseNamespace should return null if namespace is empty', (t) => { 392 | const result = logger.parseNamespace('') 393 | t.deepEqual(result, null) 394 | }) 395 | -------------------------------------------------------------------------------- /test/output_adapters.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-disable no-shadow */ 3 | 4 | const test = require('ava') 5 | const sinon = require('sinon') 6 | 7 | const outputAdapters = require('../src/output_adapters.ts') 8 | const logger = require('../src/index.ts') 9 | 10 | const stdoutWrite = process.stdout.write 11 | 12 | test.beforeEach((t) => { 13 | process.stdout.write = () => {} 14 | logger.setOutput([]) 15 | }) 16 | 17 | test.afterEach((t) => { 18 | process.stdout.write = stdoutWrite 19 | }) 20 | 21 | test('JSON Output adapter should write a Json Object with expected data and an \\n to stdout if enabled', (t) => { 22 | const now = new Date() 23 | 24 | const spy = sinon.spy(process.stdout, 'write') 25 | 26 | const log = { 27 | level: 'warn', 28 | namespace: 'test1', 29 | time: now, 30 | contextId: 'ctxId', 31 | meta: { field1: 'value1' }, 32 | message: 'test', 33 | data: { someData: 'someValue' }, 34 | } 35 | 36 | outputAdapters.json({ ...log }) 37 | 38 | t.true(spy.calledTwice) 39 | const firstCall = spy.firstCall.args[0] 40 | const secondCall = spy.secondCall.args[0] 41 | const parsedObject = JSON.parse(firstCall) 42 | 43 | t.is(parsedObject.namespace, log.namespace) 44 | t.is(parsedObject.level, log.level) 45 | t.is(parsedObject.time, log.time.toISOString()) 46 | t.is(parsedObject.contextId, log.contextId) 47 | t.is(parsedObject.field1, log.meta.field1) 48 | t.is(parsedObject.message, log.message) 49 | t.deepEqual(parsedObject.data, log.data) 50 | t.is(secondCall, '\n') 51 | 52 | process.stdout.write.restore() 53 | }) 54 | 55 | test('JSON Output Adpater should work if used by logger', (t) => { 56 | const now = new Date() 57 | 58 | logger.setNamespaces('test:*') 59 | logger.setLevel('info') 60 | logger.setOutput(outputAdapters.json) 61 | 62 | const spy = sinon.spy(process.stdout, 'write') 63 | const timersStub = sinon.useFakeTimers(now.getTime()) 64 | 65 | const log = logger.createLogger('test:subTest') 66 | log.warn('ctxId', 'test', { someData: 'someValue' }) 67 | 68 | t.true(spy.calledTwice) 69 | 70 | const firstCall = spy.firstCall.args[0] 71 | const secondCall = spy.secondCall.args[0] 72 | const parsedObject = JSON.parse(firstCall) 73 | 74 | t.is(parsedObject.namespace, 'test:subTest') 75 | t.is(parsedObject.level, 'warn') 76 | t.is(parsedObject.time, now.toISOString()) 77 | t.is(parsedObject.contextId, 'ctxId') 78 | t.is(parsedObject.message, 'test') 79 | t.deepEqual(parsedObject.data, { someData: 'someValue' }) 80 | t.is(secondCall, '\n') 81 | 82 | process.stdout.write.restore() 83 | timersStub.restore() 84 | }) 85 | 86 | test('pretty output adapter should write yaml like data and an \\n to stdout if enabled', (t) => { 87 | const spy = sinon.spy(process.stdout, 'write') 88 | 89 | const log = { 90 | level: 'warn', 91 | namespace: 'test1', 92 | time: new Date(1547205226232), 93 | contextId: 'ctxId', 94 | meta: { field1: 'value1' }, 95 | message: 'test', 96 | data: { someData: 'someValue' }, 97 | } 98 | 99 | outputAdapters.pretty({ ...log }) 100 | 101 | t.true(spy.calledTwice) 102 | 103 | const firstCall = spy.firstCall.args[0] 104 | const secondCall = spy.secondCall.args[0] 105 | 106 | let expected = `${outputAdapters.prettyTime(log.time)} (test1) [warn] : \u001b[33mtest\u001b[39m\n` 107 | expected += '\u001b[32m contextId: \u001b[39mctxId\n' 108 | expected += '\u001b[32m meta: \u001b[39m\n' 109 | expected += '\u001b[32m field1: \u001b[39mvalue1\n' 110 | expected += '\u001b[32m data: \u001b[39m\n' 111 | expected += '\u001b[32m someData: \u001b[39msomeValue\n' 112 | 113 | //@TODO: compare string with color of ava is not working with github actions, to fix this 114 | if (!process.env.CI) t.is(firstCall, expected) 115 | t.is(secondCall, '\n') 116 | 117 | process.stdout.write.restore() 118 | }) 119 | 120 | test('pretty output adapter should work if used by logger', (t) => { 121 | logger.setNamespaces('test:*') 122 | logger.setLevel('info') 123 | logger.setOutput(outputAdapters.pretty) 124 | 125 | const spy = sinon.spy(process.stdout, 'write') 126 | const timersStub = sinon.useFakeTimers(1547205226232) 127 | 128 | const log = logger.createLogger('test:subTest') 129 | log.warn('ctxId', 'test', { someData: 'someValue' }) 130 | 131 | t.true(spy.calledTwice) 132 | 133 | const firstCall = spy.firstCall.args[0] 134 | const secondCall = spy.secondCall.args[0] 135 | 136 | let expected = `${outputAdapters.prettyTime(new Date(1547205226232))} (test:subTest) [warn] : \u001b[33mtest\u001b[39m\n` 137 | expected += '\u001b[32m contextId: \u001b[39mctxId\n' 138 | expected += '\u001b[32m meta: \u001b[39m\n' 139 | expected += '\u001b[32m data: \u001b[39m\n' 140 | expected += '\u001b[32m someData: \u001b[39msomeValue\n' 141 | 142 | //@TODO: compare string with color of ava is not working with github actions, to fix this 143 | if (!process.env.CI) t.is(firstCall, expected) 144 | t.is(secondCall, '\n') 145 | 146 | process.stdout.write.restore() 147 | timersStub.restore() 148 | }) 149 | -------------------------------------------------------------------------------- /test/output_utils.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { errorToJson, stringify } = require('../src/output_utils.ts') 3 | 4 | test('errorToJson should expose error stack through a json stringify', (t) => { 5 | const e = new Error() 6 | const parsed = errorToJson(e) 7 | t.is(parsed.stack, e.stack) 8 | }) 9 | 10 | test('stringify should work even with circular references', (t) => { 11 | const obj = { 12 | a: '1', 13 | b: '2', 14 | } 15 | obj.d = obj 16 | 17 | t.notThrows(() => stringify(obj)) 18 | const value = stringify(obj) 19 | t.is(value, '{"a":"1","b":"2"}') 20 | }) 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "noImplicitAny": true, 5 | "allowJs": true, 6 | "target": "es2019", 7 | "module": "commonjs", 8 | "lib": ["es2020"], 9 | "alwaysStrict": true, 10 | "skipLibCheck": true, 11 | "noUnusedParameters": false, 12 | "noUnusedLocals": false, 13 | "strictNullChecks": true, 14 | "noUncheckedIndexedAccess": true, 15 | "esModuleInterop": true, 16 | "allowSyntheticDefaultImports": true, 17 | "forceConsistentCasingInFileNames": true, 18 | }, 19 | "include": ["src/**/*","test/**/*.ts"], 20 | "exclude": [] 21 | } -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "lib", 6 | "sourceMap": true 7 | }, 8 | "include": ["src/**/*.ts"] 9 | } 10 | --------------------------------------------------------------------------------