├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── dependabot.yml └── workflows │ ├── build-lint-test.yml │ ├── on-pr-master.yml │ ├── on-push-master.yml │ ├── on-release.yml │ ├── publish-alpha.yml │ ├── publish-coverage.yml │ └── publish-with-git-tag-version.yml ├── .gitignore ├── .mergify.yml ├── .prettierrc ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __tests__ ├── assign.spec.ts ├── errors.spec.ts ├── extending.spec.ts ├── get-logger-token.spec.ts ├── levels.spec.ts ├── logger-error-interceptor.spec.ts ├── logger-instance.spec.ts ├── module.spec.ts ├── no-context.spec.ts ├── rename-context.spec.ts ├── routing.spec.ts ├── runtime-update.spec.ts ├── use-existing.spec.ts └── utils │ ├── get-free-port.ts │ ├── logs.ts │ ├── platforms.ts │ └── test-case.ts ├── eslint.config.cjs ├── example ├── app.controller.ts ├── app.module.ts ├── main.ts └── my.service.ts ├── jest.config.js ├── logo.png ├── package-lock.json ├── package.json ├── postinstall.js ├── src ├── InjectPinoLogger.ts ├── Logger.ts ├── LoggerErrorInterceptor.ts ├── LoggerModule.ts ├── PinoLogger.ts ├── index.ts ├── params.ts └── storage.ts ├── tsconfig.build.json └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: iamolegga 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | - [ ] I've read [the docs of nestjs-pino](https://github.com/iamolegga/nestjs-pino/blob/master/README.md) 14 | - [ ] I've read [the docs of pino](https://getpino.io/#/) and [pino-http](https://github.com/pinojs/pino-http) 15 | - [ ] I couldn't find the same [open issue of nestjs-pino](https://github.com/iamolegga/nestjs-pino/issues) 16 | - [ ] This bug is related only to current library or I've tested the same behaviour with the built-in logger and this is not a bug of NestJS. 17 | 18 | **What is the current behavior?** 19 | 20 | **What is the expected behavior?** 21 | 22 | **Please provide minimal example repo, not code snippet. Without example repo this issue will be closed.** 23 | 24 | **Please mention other relevant information such as Node.js version and Operating System.** 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE REQUEST]" 5 | labels: enhancement 6 | assignees: iamolegga 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Please ask your question 4 | title: "[QUESTION]" 5 | labels: question 6 | assignees: iamolegga 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | 14 | - [ ] I've read [the docs of nestjs-pino](https://github.com/iamolegga/nestjs-pino/blob/master/README.md) 15 | - [ ] I've read [the docs of pino](https://getpino.io/#/) and [pino-http](https://github.com/pinojs/pino-http) 16 | - [ ] I couldn't find the same [question about nestjs-pino](https://github.com/iamolegga/nestjs-pino/issues?q=label%3Aquestion) 17 | - [ ] This is not a question about how to use this library with pino-_transport-name-goes-here_ 18 | 19 | **Question** 20 | 21 | 22 | 23 | **Please mention other relevant information such as Node.js version and Operating System.** 24 | 25 | 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "02:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: "reflect-metadata" 11 | update-types: ["version-update:semver-major", "version-update:semver-minor"] 12 | -------------------------------------------------------------------------------- /.github/workflows/build-lint-test.yml: -------------------------------------------------------------------------------- 1 | name: build-lint-test 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | build-lint-test: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | nestjs-version: 12 | - "8" 13 | - "9" 14 | - "10" 15 | - "11" 16 | nodejs-version: 17 | - 20 18 | - 22 19 | include: 20 | - nestjs-version: 8 21 | typescript-version: 4 22 | - nestjs-version: 9 23 | typescript-version: 4 24 | - nestjs-version: 10 25 | typescript-version: 5 26 | - nestjs-version: 11 27 | typescript-version: 5 28 | 29 | runs-on: ubuntu-latest 30 | 31 | steps: 32 | - uses: actions/checkout@v3 33 | with: 34 | fetch-depth: 0 35 | 36 | - uses: actions/setup-node@v3 37 | with: 38 | node-version: ${{ matrix.nodejs-version }} 39 | - run: npm ci 40 | - run: | 41 | npm i @nestjs/core@${{ matrix.nestjs-version }} \ 42 | @nestjs/common@${{ matrix.nestjs-version }} \ 43 | @nestjs/platform-express@${{ matrix.nestjs-version }} \ 44 | @nestjs/platform-fastify@${{ matrix.nestjs-version }} \ 45 | @nestjs/testing@${{ matrix.nestjs-version }} \ 46 | @types/node@${{ matrix.nodejs-version }} \ 47 | typescript@${{ matrix.typescript-version }} \ 48 | -D 49 | - run: npm run lint 50 | - uses: actions/cache@v3 51 | with: 52 | path: coverage 53 | key: ${{ github.sha }}-${{ matrix.nestjs-version }}-${{ matrix.nodejs-version }} 54 | - run: npm t 55 | - run: npm run build 56 | -------------------------------------------------------------------------------- /.github/workflows/on-pr-master.yml: -------------------------------------------------------------------------------- 1 | name: on-pr-master 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-lint-test: 10 | uses: ./.github/workflows/build-lint-test.yml 11 | secrets: inherit 12 | -------------------------------------------------------------------------------- /.github/workflows/on-push-master.yml: -------------------------------------------------------------------------------- 1 | name: on-push 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-lint-test: 10 | uses: ./.github/workflows/build-lint-test.yml 11 | secrets: inherit 12 | 13 | publish-alpha: 14 | if: github.actor != 'dependabot[bot]' && github.actor != 'mergify[bot]' 15 | needs: 16 | - build-lint-test 17 | uses: ./.github/workflows/publish-alpha.yml 18 | secrets: inherit 19 | 20 | coverage: 21 | needs: 22 | - build-lint-test 23 | uses: ./.github/workflows/publish-coverage.yml 24 | secrets: inherit 25 | -------------------------------------------------------------------------------- /.github/workflows/on-release.yml: -------------------------------------------------------------------------------- 1 | name: on-release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish-with-git-tag-version: 9 | uses: ./.github/workflows/publish-with-git-tag-version.yml 10 | secrets: inherit 11 | -------------------------------------------------------------------------------- /.github/workflows/publish-alpha.yml: -------------------------------------------------------------------------------- 1 | name: publish-alpha 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | publish-alpha: 8 | if: github.ref == 'refs/heads/master' && github.actor != 'dependabot[bot]' && github.actor != 'mergify[bot]' 9 | runs-on: ubuntu-latest 10 | steps: 11 | 12 | - uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 20 19 | registry-url: 'https://registry.npmjs.org' 20 | 21 | - run: npm ci 22 | 23 | - run: npm version --no-git-tag-version $(git describe --abbrev=0 --tags)-alpha.$(git rev-parse --short=6 ${{ github.sha }}) || true 24 | 25 | - run: npm publish --tag alpha || true 26 | env: 27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/publish-coverage.yml: -------------------------------------------------------------------------------- 1 | name: publish-coverage 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | publish-coverage: 8 | runs-on: ubuntu-latest 9 | steps: 10 | 11 | - uses: actions/checkout@v3 12 | with: 13 | fetch-depth: 0 14 | 15 | - uses: actions/cache@v3 16 | with: 17 | path: coverage 18 | # restore cache from build-lint-test wf with key=sha-(oneof nestjs version)-(oneof nodejs version) 19 | key: ${{ github.sha }}-11-22 20 | 21 | - uses: paambaati/codeclimate-action@v2.7.5 22 | with: 23 | coverageLocations: ${{github.workspace}}/coverage/lcov.info:lcov 24 | env: 25 | CC_TEST_REPORTER_ID: ${{ secrets.CODE_CLIMATE_REPORTER_ID }} 26 | -------------------------------------------------------------------------------- /.github/workflows/publish-with-git-tag-version.yml: -------------------------------------------------------------------------------- 1 | name: publish-with-git-tag-version 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | publish-with-git-tag-version: 8 | runs-on: ubuntu-latest 9 | steps: 10 | 11 | - uses: actions/checkout@v3 12 | with: 13 | fetch-depth: 0 14 | 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 20 18 | registry-url: 'https://registry.npmjs.org' 19 | 20 | - run: npm ci 21 | 22 | - run: npm version --no-git-tag-version $(git describe --abbrev=0 --tags) 23 | 24 | - run: | 25 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 26 | git config --local user.name "github-actions[bot]" 27 | git commit -m "update version" -a 28 | 29 | - uses: ad-m/github-push-action@master 30 | with: 31 | github_token: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - run: npm publish 34 | env: 35 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: dependabot-nestjs-pino 3 | queue_conditions: 4 | - author~=^dependabot(|-preview)\[bot\]$ 5 | - -check-failure~=build-lint-test 6 | - check-success~=build-lint-test 7 | - check-success=security/snyk (iamolegga) 8 | merge_conditions: 9 | - author~=^dependabot(|-preview)\[bot\]$ 10 | - -check-failure~=build-lint-test 11 | - check-success~=build-lint-test 12 | - check-success=security/snyk (iamolegga) 13 | merge_method: rebase 14 | 15 | pull_request_rules: 16 | - name: refactored queue action rule 17 | conditions: [] 18 | actions: 19 | queue: 20 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at iamolegga@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | All pull requests that respect next rules are welcome: 2 | 3 | - Before opening pull request to this repo run `npm t` to run tests. 4 | - All bugfixes and features should contain tests and coverage must be 100% 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 iamolegga 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Bombed Vovchansk, Ukraine 3 |
4 | "Vovchansk (2024-06-02) 1513" by National Police of Ukraine (Liut Brigade) is licensed under CC BY 4.0. 5 |

6 |

7 | This is Vovchansk, Ukraine, the city where the father of this library’s author was born. This is how it looks now, after the Russian invasion. If you find this library useful and would like to thank the author, please consider donating any amount via one of the following links: 8 |
9 | ・Armed Forces of Ukraine"The Come Back Alive" foundation・ 10 |
11 | Thanks for your support! 🇺🇦 12 |

13 | 14 |

NestJS-Pino

15 | 16 |

17 | 18 | npm 19 | 20 | 21 | npm 22 | 23 | 24 | GitHub branch checks state 25 | 26 | 27 | 28 | 29 | 30 | Known Vulnerabilities 31 | 32 | 33 | Libraries.io 34 | 35 | Dependabot 36 | Supported platforms: Express & Fastify 37 |

38 | 39 |

✨✨✨ Platform agnostic logger for NestJS based on Pino with REQUEST CONTEXT IN EVERY LOG ✨✨✨

40 | 41 | --- 42 | 43 |

This is documentation for v2+ which works with NestJS 8+.
Please see documentation for the previous major version which works with NestJS < 8 here.

44 | 45 | --- 46 | 47 | ## Install 48 | 49 | ```sh 50 | npm i nestjs-pino pino-http 51 | ``` 52 | 53 | ## Example 54 | 55 | Firstly, import module with `LoggerModule.forRoot(...)` or `LoggerModule.forRootAsync(...)` only once in root module (check out module configuration docs [below](#configuration)): 56 | 57 | ```ts 58 | import { LoggerModule } from 'nestjs-pino'; 59 | 60 | @Module({ 61 | imports: [LoggerModule.forRoot()], 62 | }) 63 | class AppModule {} 64 | ``` 65 | 66 | Secondly, set up app logger: 67 | 68 | ```ts 69 | import { Logger } from 'nestjs-pino'; 70 | 71 | const app = await NestFactory.create(AppModule, { bufferLogs: true }); 72 | app.useLogger(app.get(Logger)); 73 | ``` 74 | 75 | Now you can use one of two loggers: 76 | 77 | ```ts 78 | // NestJS standard built-in logger. 79 | // Logs will be produced by pino internally 80 | import { Logger } from '@nestjs/common'; 81 | 82 | export class MyService { 83 | private readonly logger = new Logger(MyService.name); 84 | foo() { 85 | // All logger methods have args format the same as pino, but pino methods 86 | // `trace` and `info` are mapped to `verbose` and `log` to satisfy 87 | // `LoggerService` interface of NestJS: 88 | this.logger.verbose({ foo: 'bar' }, 'baz %s', 'qux'); 89 | this.logger.debug('foo %s %o', 'bar', { baz: 'qux' }); 90 | this.logger.log('foo'); 91 | } 92 | } 93 | ``` 94 | 95 | Usage of the standard logger is recommended and idiomatic for NestJS. But there is one more option to use: 96 | 97 | ```ts 98 | import { PinoLogger, InjectPinoLogger } from 'nestjs-pino'; 99 | 100 | export class MyService { 101 | constructor( 102 | private readonly logger: PinoLogger 103 | ) { 104 | // Optionally you can set context for logger in constructor or ... 105 | this.logger.setContext(MyService.name); 106 | } 107 | 108 | constructor( 109 | // ... set context via special decorator 110 | @InjectPinoLogger(MyService.name) 111 | private readonly logger: PinoLogger 112 | ) {} 113 | 114 | foo() { 115 | // PinoLogger has same methods as pino instance 116 | this.logger.trace({ foo: 'bar' }, 'baz %s', 'qux'); 117 | this.logger.debug('foo %s %o', 'bar', { baz: 'qux' }); 118 | this.logger.info('foo'); 119 | } 120 | } 121 | ``` 122 | 123 | Output: 124 | 125 | ```json 126 | // Logs by app itself 127 | {"level":30,"time":1629823318326,"pid":14727,"hostname":"my-host","context":"NestFactory","msg":"Starting Nest application..."} 128 | {"level":30,"time":1629823318326,"pid":14727,"hostname":"my-host","context":"InstanceLoader","msg":"LoggerModule dependencies initialized"} 129 | {"level":30,"time":1629823318327,"pid":14727,"hostname":"my-host","context":"InstanceLoader","msg":"AppModule dependencies initialized"} 130 | {"level":30,"time":1629823318327,"pid":14727,"hostname":"my-host","context":"RoutesResolver","msg":"AppController {/}:"} 131 | {"level":30,"time":1629823318327,"pid":14727,"hostname":"my-host","context":"RouterExplorer","msg":"Mapped {/, GET} route"} 132 | {"level":30,"time":1629823318327,"pid":14727,"hostname":"my-host","context":"NestApplication","msg":"Nest application successfully started"} 133 | 134 | // Logs by injected Logger and PinoLogger in Services/Controllers. Every log 135 | // has it's request data and unique `req.id` (by default id is unique per 136 | // process, but you can set function to generate it from request context and 137 | // for example pass here incoming `X-Request-ID` header or generate UUID) 138 | {"level":10,"time":1629823792023,"pid":15067,"hostname":"my-host","req":{"id":1,"method":"GET","url":"/","query":{},"params":{"0":""},"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"},"remoteAddress":"::1","remotePort":63822},"context":"MyService","foo":"bar","msg":"baz qux"} 139 | {"level":20,"time":1629823792023,"pid":15067,"hostname":"my-host","req":{"id":1,"method":"GET","url":"/","query":{},"params":{"0":""},"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"},"remoteAddress":"::1","remotePort":63822},"context":"MyService","msg":"foo bar {\"baz\":\"qux\"}"} 140 | {"level":30,"time":1629823792023,"pid":15067,"hostname":"my-host","req":{"id":1,"method":"GET","url":"/","query":{},"params":{"0":""},"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"},"remoteAddress":"::1","remotePort":63822},"context":"MyService","msg":"foo"} 141 | 142 | // Automatic logs of every request/response 143 | {"level":30,"time":1629823792029,"pid":15067,"hostname":"my-host","req":{"id":1,"method":"GET","url":"/","query":{},"params":{"0":""},"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"},"remoteAddress":"::1","remotePort":63822},"res":{"statusCode":200,"headers":{"x-powered-by":"Express","content-type":"text/html; charset=utf-8","content-length":"12","etag":"W/\"c-Lve95gjOVATpfV8EL5X4nxwjKHE\""}},"responseTime":7,"msg":"request completed"} 144 | ``` 145 | 146 | ## Comparison with others 147 | 148 | There are other Nestjs loggers. Key purposes of this module are: 149 | 150 | - to be idiomatic NestJS logger 151 | - to log in JSON format (thanks to [pino](https://getpino.io/#/) - [super fast logger](https://github.com/pinojs/pino/blob/master/docs/benchmarks.md)) [why](https://www.loggly.com/use-cases/json-logging-best-practices/) [you](https://hackernoon.com/log-everything-as-json-hmq32ax) [should](https://blog.treasuredata.com/blog/2012/04/26/log-everything-as-json/) [use](https://stackify.com/what-is-structured-logging-and-why-developers-need-it/) [JSON](https://softwareengineering.stackexchange.com/a/170528) 152 | - to log every request/response automatically (thanks to [pino-http](https://github.com/pinojs/pino-http#pino-http)) 153 | - to bind request data to the logs automatically from any service on any application layer without passing request context (thanks to [AsyncLocalStorage](https://nodejs.org/api/async_context.html#async_context_class_asynclocalstorage)) 154 | - to have another alternative logger with same API as `pino` instance (`PinoLogger`) for experienced `pino` users to make more comfortable usage. 155 | 156 | | Logger | Nest App logger | Logger service | Auto-bind request data to logs | 157 | | ------------------ | :-------------: | :------------: | :---------------------------: | 158 | | nest-winston | + | + | - | 159 | | nestjs-pino-logger | + | + | - | 160 | | **nestjs-pino** | + | + | + | 161 | 162 | ## Configuration 163 | 164 | ### Zero configuration 165 | 166 | Just import `LoggerModule` to your module: 167 | 168 | ```ts 169 | import { LoggerModule } from 'nestjs-pino'; 170 | 171 | @Module({ 172 | imports: [LoggerModule.forRoot()], 173 | ... 174 | }) 175 | class MyModule {} 176 | ``` 177 | 178 | ### Configuration params 179 | 180 | The following interface is using for the configuration: 181 | 182 | ```ts 183 | interface Params { 184 | /** 185 | * Optional parameters for `pino-http` module 186 | * @see https://github.com/pinojs/pino-http#api 187 | */ 188 | pinoHttp?: 189 | | pinoHttp.Options 190 | | DestinationStream 191 | | [pinoHttp.Options, DestinationStream]; 192 | 193 | /** 194 | * Optional parameter for routing. It should implement interface of 195 | * parameters of NestJS built-in `MiddlewareConfigProxy['forRoutes']`. 196 | * @see https://docs.nestjs.com/middleware#applying-middleware 197 | * It can be used for both disabling automatic req/res logs (see above) and 198 | * removing request context from following logs. It works for all requests by 199 | * default. If you only need to turn off the automatic request/response 200 | * logging for some specific (or all) routes but keep request context for app 201 | * logs use `pinoHttp.autoLogging` field. 202 | */ 203 | forRoutes?: Parameters; 204 | 205 | /** 206 | * Optional parameter for routing. It should implement interface of 207 | * parameters of NestJS built-in `MiddlewareConfigProxy['exclude']`. 208 | * @see https://docs.nestjs.com/middleware#applying-middleware 209 | * It can be used for both disabling automatic req/res logs (see above) and 210 | * removing request context from following logs. It works for all requests by 211 | * default. If you only need to turn off the automatic request/response 212 | * logging for some specific (or all) routes but keep request context for app 213 | * logs use `pinoHttp.autoLogging` field. 214 | */ 215 | exclude?: Parameters; 216 | 217 | /** 218 | * Optional parameter to skip pino configuration in case you are using 219 | * FastifyAdapter, and already configure logger in adapter's config. The Pros 220 | * and cons of this approach are described in the FAQ section of the 221 | * documentation: 222 | * @see https://github.com/iamolegga/nestjs-pino#faq. 223 | */ 224 | useExisting?: true; 225 | 226 | /** 227 | * Optional parameter to change property name `context` in resulted logs, 228 | * so logs will be like: 229 | * {"level":30, ... "RENAME_CONTEXT_VALUE_HERE":"AppController" } 230 | */ 231 | renameContext?: string; 232 | } 233 | ``` 234 | 235 | ### Synchronous configuration 236 | 237 | Use `LoggerModule.forRoot` method with argument of [Params interface](#configuration-params): 238 | 239 | ```ts 240 | import { LoggerModule } from 'nestjs-pino'; 241 | 242 | @Module({ 243 | imports: [ 244 | LoggerModule.forRoot({ 245 | pinoHttp: [ 246 | { 247 | name: 'add some name to every JSON line', 248 | level: process.env.NODE_ENV !== 'production' ? 'debug' : 'info', 249 | // install 'pino-pretty' package in order to use the following option 250 | transport: process.env.NODE_ENV !== 'production' 251 | ? { target: 'pino-pretty' } 252 | : undefined, 253 | 254 | 255 | // and all the other fields of: 256 | // - https://github.com/pinojs/pino-http#api 257 | // - https://github.com/pinojs/pino/blob/HEAD/docs/api.md#options-object 258 | 259 | 260 | }, 261 | someWritableStream 262 | ], 263 | forRoutes: [MyController], 264 | exclude: [{ method: RequestMethod.ALL, path: 'check' }] 265 | }) 266 | ], 267 | ... 268 | }) 269 | class MyModule {} 270 | ``` 271 | 272 | ### Asynchronous configuration 273 | 274 | With `LoggerModule.forRootAsync` you can, for example, import your `ConfigModule` and inject `ConfigService` to use it in `useFactory` method. 275 | 276 | `useFactory` should return object with [Params interface](#configuration-params) or undefined 277 | 278 | Here's an example: 279 | 280 | ```ts 281 | import { LoggerModule } from 'nestjs-pino'; 282 | 283 | @Injectable() 284 | class ConfigService { 285 | public readonly level = 'debug'; 286 | } 287 | 288 | @Module({ 289 | providers: [ConfigService], 290 | exports: [ConfigService] 291 | }) 292 | class ConfigModule {} 293 | 294 | @Module({ 295 | imports: [ 296 | LoggerModule.forRootAsync({ 297 | imports: [ConfigModule], 298 | inject: [ConfigService], 299 | useFactory: async (config: ConfigService) => { 300 | await somePromise(); 301 | return { 302 | pinoHttp: { level: config.level }, 303 | }; 304 | } 305 | }) 306 | ], 307 | ... 308 | }) 309 | class TestModule {} 310 | ``` 311 | 312 | ### Asynchronous logging 313 | 314 | > In essence, asynchronous logging enables even faster performance by `pino`. 315 | 316 | Please, read [pino asynchronous mode docs](https://github.com/pinojs/pino/blob/master/docs/asynchronous.md) first. There is a possibility of the most recently buffered log messages being lost in case of a system failure, e.g. a power cut. 317 | 318 | If you know what you're doing, you can enable it like so: 319 | 320 | ```ts 321 | import pino from 'pino'; 322 | import { LoggerModule } from 'nestjs-pino'; 323 | 324 | @Module({ 325 | imports: [ 326 | LoggerModule.forRoot({ 327 | pinoHttp: { 328 | stream: pino.destination({ 329 | dest: './my-file', // omit for stdout 330 | minLength: 4096, // Buffer before writing 331 | sync: false, // Asynchronous logging 332 | }), 333 | }, 334 | }), 335 | ], 336 | ... 337 | }) 338 | class MyModule {} 339 | ``` 340 | 341 | See [pino.destination](https://github.com/pinojs/pino/blob/master/docs/api.md#pino-destination) 342 | 343 | ## Testing a class that uses @InjectPinoLogger 344 | 345 | This package exposes a `getLoggerToken()` function that returns a prepared injection token based on the provided context. 346 | Using this token, you can provide a mock implementation of the logger using any of the standard custom provider techniques, including `useClass`, `useValue` and `useFactory`. 347 | 348 | ```ts 349 | const module: TestingModule = await Test.createTestingModule({ 350 | providers: [ 351 | MyService, 352 | { 353 | provide: getLoggerToken(MyService.name), 354 | useValue: mockLogger, 355 | }, 356 | ], 357 | }).compile(); 358 | ``` 359 | 360 | ## Logger/PinoLogger class extension 361 | 362 | `Logger` and `PinoLogger` classes can be extended. 363 | 364 | ```ts 365 | // logger.service.ts 366 | import { Logger, PinoLogger, Params, PARAMS_PROVIDER_TOKEN } from 'nestjs-pino'; 367 | 368 | @Injectable() 369 | class LoggerService extends Logger { 370 | constructor( 371 | logger: PinoLogger, 372 | @Inject(PARAMS_PROVIDER_TOKEN) params: Params 373 | ) { 374 | ... 375 | } 376 | // extended method 377 | myMethod(): any {} 378 | } 379 | 380 | import { PinoLogger, Params, PARAMS_PROVIDER_TOKEN } from 'nestjs-pino'; 381 | 382 | @Injectable() 383 | class LoggerService extends PinoLogger { 384 | constructor( 385 | @Inject(PARAMS_PROVIDER_TOKEN) params: Params 386 | ) { 387 | // ... 388 | } 389 | // extended method 390 | myMethod(): any {} 391 | } 392 | 393 | 394 | // logger.module.ts 395 | @Module({ 396 | providers: [LoggerService], 397 | exports: [LoggerService], 398 | imports: [LoggerModule.forRoot()], 399 | }) 400 | class LoggerModule {} 401 | ``` 402 | 403 | ## Notes on `Logger` injection in constructor 404 | 405 | Since logger substitution has appeared in NestJS@8 the main purpose of `Logger` class is to be registered via `app.useLogger(app.get(Logger))`. But that requires some internal breaking change, because with such usage NestJS pass logger's context as the last optional argument in logging function. So in current version `Logger`'s methods accept context as a last argument. 406 | 407 | With such change it's not possible to detect if method was called by app internaly and the last argument is context or `Logger` was injected in some service via `constructor(private logger: Logger) {}` and the last argument is interpolation value for example. 408 | 409 | ## Assign extra fields for future calls 410 | 411 | You can enrich logs before calling log methods. It's possible by using `assign` method of `PinoLogger` instance. As `Logger` class is used only for NestJS built-in `Logger` substitution via `app.useLogger(...)` this feature is only limited to `PinoLogger` class. Example: 412 | 413 | ```ts 414 | 415 | @Controller('/') 416 | class TestController { 417 | constructor( 418 | private readonly logger: PinoLogger, 419 | private readonly service: MyService, 420 | ) {} 421 | 422 | @Get() 423 | get() { 424 | // assign extra fields in one place... 425 | this.logger.assign({ userID: '42' }); 426 | return this.service.test(); 427 | } 428 | } 429 | 430 | @Injectable() 431 | class MyService { 432 | private readonly logger = new Logger(MyService.name); 433 | 434 | test() { 435 | // ...and it will be logged in another one 436 | this.logger.log('hello world'); 437 | } 438 | } 439 | ``` 440 | 441 | By default, this does not extend `Request completed` logs. Set the `assignResponse` parameter to `true` to also enrich response logs automatically emitted by `pino-http`. 442 | 443 | ## Change pino params at runtime 444 | 445 | Pino root instance with passed via module registration params creates a separate child logger for every request. This root logger params can be changed at runtime via `PinoLogger.root` property which is the pointer to logger instance. Example: 446 | 447 | ```ts 448 | @Controller('/') 449 | class TestController { 450 | @Post('/change-loggin-level') 451 | setLevel() { 452 | PinoLogger.root.level = 'info'; 453 | return null; 454 | } 455 | } 456 | ``` 457 | 458 | ## Expose stack trace and error class in `err` property 459 | 460 | By default, `pino-http` exposes `err` property with a stack trace and error details, however, this `err` property contains default error details, which do not tell anything about actual error. To expose actual error details you need you to use a NestJS interceptor which captures exceptions and assigns them to the response object `err` property which is later processed by pino-http: 461 | 462 | ```typescript 463 | import { LoggerErrorInterceptor } from 'nestjs-pino'; 464 | 465 | const app = await NestFactory.create(AppModule); 466 | app.useGlobalInterceptors(new LoggerErrorInterceptor()); 467 | ``` 468 | 469 | ## Migration 470 | 471 | ### v1 472 | 473 | - All parameters of v.0 are moved to `pinoHttp` property (except `useExisting`). 474 | - `useExisting` now accept only `true` because you should already know if you want to use preconfigured fastify adapter's logger (and set `true`) or not (and just not define this field). 475 | 476 | ### v2 477 | 478 | #### Logger substitution 479 | 480 | A new more convenient way to inject a custom logger that implements `LoggerService` has appeared in recent versions of NestJS (mind the `bufferLogs` field, it will force NestJS to wait for logger to be ready instead of using built-in logger on start): 481 | 482 | ```ts 483 | // main.ts 484 | import { Logger } from 'nestjs-pino'; 485 | // ... 486 | const app = await NestFactory.create(AppModule, { bufferLogs: true }); 487 | app.useLogger(app.get(Logger)); 488 | // ... 489 | ``` 490 | 491 | Note that for [standalone applications](https://docs.nestjs.com/standalone-applications), buffering has to be [flushed using app.flushLogs()](https://github.com/nestjs/nest/blob/24e6c821a0859448646fd88831f20e4c5ae50980/packages/core/nest-application-context.ts#L136) manually after custom logger is ready to be used by NestJS (refer to [this issue](https://github.com/iamolegga/nestjs-pino/issues/553) for more details): 492 | 493 | ```ts 494 | // main.ts 495 | import { Logger } from 'nestjs-pino'; 496 | 497 | // ... 498 | const app = await NestFactory.createApplicationContext(AppModule, { bufferLogs: true }); 499 | app.useLogger(app.get(Logger)); 500 | app.flushLogs(); 501 | // ... 502 | ``` 503 | 504 | In all the other places you can use built-in `Logger`: 505 | 506 | ```ts 507 | // my-service.ts 508 | import { Logger } from '@nestjs/common'; 509 | class MyService { 510 | private readonly logger = new Logger(MyService.name); 511 | } 512 | ``` 513 | 514 | To quote the official docs: 515 | 516 | > If we supply a custom logger via `app.useLogger()`, it will actually be used by Nest internally. That means that our code remains implementation agnostic, while we can easily substitute the default logger for our custom one by calling `app.useLogger()`. 517 | > 518 | > That way if we follow the steps from the previous section and call `app.useLogger(app.get(MyLogger))`, the following calls to `this.logger.log()` from `MyService` would result in calls to method `log` from `MyLogger` instance. 519 | 520 | --- 521 | 522 | **This is recommended to update all your existing `Logger` injections from `nestjs-pino` to `@nestjs/common`. And inject it only in your `main.ts` file as shown above. Support of injection of `Logger` (don't confuse with `PinoLogger`) from `nestjs-pino` directly in class constructors is dropped.** 523 | 524 | --- 525 | 526 | Since logger substitution has appeared the main purpose of `Logger` class is to be registered via `app.useLogger(app.get(Logger))`. But that requires some internal breaking change, because with such usage NestJS pass logger's context as the last optional argument in logging function. So in current version `Logger`'s methods accept context as the last argument. 527 | 528 | With such change it's not possible to detect if method was called by app internaly and the last argument is context or `Logger` was injected in some service via `constructor(private logger: Logger) {}` and the last argument is interpolation value for example. That's why logging with such injected class still works, but only for 1 argument. 529 | 530 | #### NestJS LoggerService interface breaking change 531 | 532 | In NestJS@8 all logging methods of built-in `LoggerService` now accept the same arguments without second `context` argument (which is set via injection, see above), for example: `log(message: any, ...optionalParams: any[]): any;`. That makes usage of built-in logger more convenient and compatible with `pino`'s logging methods. So this is a breaking change in NestJS, and you should be aware of it. 533 | 534 | In NestJS <= 7 and `nestjs-pino@1` when you call `this.logger.log('foo', 'bar');` there would be such log: `{..."context":"bar","msg":"foo"}` (second argument goes to `context` field by desing). In NestJS 8 and `nestjs-pino@2` (with proper injection that shown above) same call will result in `{..."context":"MyService","msg":"foo"}`, so `context` is passed via injection, but second argument disappear from log, because now it treats as interpolation value and there should be placeholder for it in `message` argument. So if you want to get both `foo` and `bar` in log the right way to do this is: `this.logger.log('foo %s', 'bar');`. More info can be found in [pino docs](https://getpino.io/#/docs/api?id=logging-method-parameters). 535 | 536 | ## FAQ 537 | 538 | **Q**: _How to disable automatic request/response logs?_ 539 | 540 | **A**: check out [autoLogging field of pino-http](https://github.com/pinojs/pino-http#pinohttpopts-stream) that are set in `pinoHttp` field of `Params` 541 | 542 | --- 543 | 544 | **Q**: _How to pass `X-Request-ID` header or generate UUID for `req.id` field of log?_ 545 | 546 | **A**: check out [genReqId field of pino-http](https://github.com/pinojs/pino-http#pinohttpopts-stream) that are set in `pinoHttp` field of `Params` 547 | 548 | --- 549 | 550 | **Q**: _How does it work?_ 551 | 552 | **A**: It uses [pino-http](https://github.com/pinojs/pino-http) under hood, so every request has it's own [child-logger](https://github.com/pinojs/pino/blob/master/docs/child-loggers.md), and with help of [AsyncLocalStorage](https://nodejs.org/api/async_context.html#async_context_class_asynclocalstorage) `Logger` and `PinoLogger` can get it while calling own methods. So your logs can be grouped by `req.id`. 553 | 554 | --- 555 | 556 | **Q**: _Why use [AsyncLocalStorage](https://nodejs.org/api/async_context.html#async_context_class_asynclocalstorage) instead of [REQUEST scope](https://docs.nestjs.com/fundamentals/injection-scopes#per-request-injection)?_ 557 | 558 | **A**: [REQUEST scope](https://docs.nestjs.com/fundamentals/injection-scopes#per-request-injection) can have [perfomance issues](https://docs.nestjs.com/fundamentals/injection-scopes#performance). TL;DR: it will have to create an instance of the class (that injects `Logger`) on each request, and that will slow down your response times. 559 | 560 | --- 561 | 562 | **Q**: _I'm using old nodejs version, will it work for me?_ 563 | 564 | **A**: Please check out [history of this feature](https://nodejs.org/api/async_context.html#async_context_class_asynclocalstorage). 565 | 566 | --- 567 | 568 | **Q**: _What about `pino` built-in methods/levels?_ 569 | 570 | **A**: Pino built-in methods names are not fully compatible with NestJS built-in `LoggerService` methods names, and there is an option which logger you use. Here is methods mapping: 571 | 572 | | `pino` method | `PinoLogger` method | NestJS built-in `Logger` method | 573 | | ------------- | ------------------- | --------------------------------| 574 | | **trace** | **trace** | **verbose** | 575 | | debug | debug | debug | 576 | | **info** | **info** | **log** | 577 | | warn | warn | warn | 578 | | error | error | error | 579 | | fatal | fatal | fatal (since nestjs@10.2) | 580 | 581 | --- 582 | 583 | **Q**: _Fastify already includes `pino`, and I want to configure it on `Adapter` level, and use this config for logger_ 584 | 585 | **A**: You can do it by providing `useExisting: true`. But there is one caveat: 586 | 587 | Fastify creates logger with your config per every request. And this logger is used by `Logger`/`PinoLogger` services inside that context underhood. 588 | 589 | But Nest Application has another contexts of execution, for example [lifecycle events](https://docs.nestjs.com/fundamentals/lifecycle-events), where you still may want to use logger. For that `Logger`/`PinoLogger` services use separate `pino` instance with config, that provided via `forRoot`/`forRootAsync` methods. 590 | 591 | So, when you want to configure `pino` via `FastifyAdapter` there is no way to get back this config from fastify and pass it to that _out of context_ logger. 592 | 593 | And if you will not pass config via `forRoot`/`forRootAsync` _out of context_ logger will be instantiated with default params. So if you want to configure it with the same options for consistency you have to provide the same config to `LoggerModule` configuration too. But if you already provide it to `LoggerModule` configuration you can drop `useExisting` field from config and drop logger configuration on `FastifyAdapter`, and it will work without code duplication. 594 | 595 | So this property (`useExisting: true`) is not recommended, and can be useful only for cases when: 596 | 597 | - this logger is not using for lifecycle events and application level logging in NestJS apps based on fastify 598 | - `pino` is using with default params in NestJS apps based on fastify 599 | 600 | All the other cases are lead to either code duplication or unexpected behavior. 601 | 602 | --- 603 | 604 |

Do you use this library?
Don't be shy to give it a star! ★

605 | 606 |

Also if you are into NestJS you might be interested in one of my other NestJS libs.

607 | -------------------------------------------------------------------------------- /__tests__/assign.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Injectable, 5 | Logger, 6 | OnModuleInit, 7 | } from '@nestjs/common'; 8 | 9 | import { PinoLogger } from '../src'; 10 | 11 | import { platforms } from './utils/platforms'; 12 | import { TestCase } from './utils/test-case'; 13 | 14 | describe('assign', () => { 15 | for (const PlatformAdapter of platforms) { 16 | describe(PlatformAdapter.name, () => { 17 | it('in request context', async () => { 18 | const msg = Math.random().toString(); 19 | 20 | @Injectable() 21 | class TestService { 22 | private readonly logger = new Logger(TestService.name); 23 | 24 | test() { 25 | this.logger.log(msg); 26 | return {}; 27 | } 28 | } 29 | 30 | @Controller('/') 31 | class TestController { 32 | constructor( 33 | private readonly logger: PinoLogger, 34 | private readonly service: TestService, 35 | ) {} 36 | 37 | @Get() 38 | get() { 39 | this.logger.assign({ foo: 'bar' }); 40 | return this.service.test(); 41 | } 42 | } 43 | 44 | const logs = await new TestCase(new PlatformAdapter(), { 45 | controllers: [TestController], 46 | providers: [TestService], 47 | }) 48 | .forRoot({ pinoHttp: { customSuccessMessage: () => 'success' } }) 49 | .run(); 50 | 51 | const wanted = logs.some((l) => l.msg === msg && l.foo === 'bar'); 52 | expect(wanted).toBeTruthy(); 53 | const responseLog = logs.find((l) => l.msg === 'success'); 54 | expect(responseLog).toBeDefined(); 55 | expect(responseLog).not.toHaveProperty('foo'); 56 | }); 57 | 58 | it('out of request context', async () => { 59 | const msg = Math.random().toString(); 60 | 61 | @Injectable() 62 | class TestService implements OnModuleInit { 63 | constructor(private readonly logger: PinoLogger) { 64 | logger.setContext(TestService.name); 65 | } 66 | 67 | onModuleInit() { 68 | expect(() => this.logger.assign({ foo: 'bar' })).toThrow(); 69 | } 70 | 71 | test() { 72 | this.logger.info(msg); 73 | return {}; 74 | } 75 | } 76 | 77 | @Controller('/') 78 | class TestController { 79 | constructor(private readonly service: TestService) {} 80 | 81 | @Get() 82 | get() { 83 | return this.service.test(); 84 | } 85 | } 86 | 87 | const logs = await new TestCase(new PlatformAdapter(), { 88 | controllers: [TestController], 89 | providers: [TestService], 90 | }) 91 | .forRoot() 92 | .run(); 93 | 94 | const log = logs.find((l) => l.msg === msg); 95 | expect(log).toBeTruthy(); 96 | expect(log).not.toHaveProperty('foo'); 97 | }); 98 | 99 | it('response log', async () => { 100 | const msg = Math.random().toString(); 101 | 102 | @Injectable() 103 | class TestService { 104 | private readonly logger = new Logger(TestService.name); 105 | 106 | test() { 107 | this.logger.log(msg); 108 | return {}; 109 | } 110 | } 111 | 112 | @Controller('/') 113 | class TestController { 114 | constructor( 115 | private readonly logger: PinoLogger, 116 | private readonly service: TestService, 117 | ) {} 118 | 119 | @Get() 120 | get() { 121 | this.logger.assign({ foo: 'bar' }); 122 | this.logger.assign({ other: 'value' }); 123 | return this.service.test(); 124 | } 125 | } 126 | 127 | const logs = await new TestCase(new PlatformAdapter(), { 128 | controllers: [TestController], 129 | providers: [TestService], 130 | }) 131 | .forRoot({ 132 | assignResponse: true, 133 | pinoHttp: { customSuccessMessage: () => 'success' }, 134 | }) 135 | .run(); 136 | 137 | const hasServiceLog = logs.some( 138 | (l) => l.msg === msg && l.foo === 'bar' && l.other === 'value', 139 | ); 140 | expect(hasServiceLog).toBeTruthy(); 141 | const hasResponseLog = logs.some( 142 | (l) => l.msg === 'success' && l.foo === 'bar' && l.other === 'value', 143 | ); 144 | expect(hasResponseLog).toBeTruthy(); 145 | }); 146 | }); 147 | } 148 | }); 149 | -------------------------------------------------------------------------------- /__tests__/errors.spec.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Logger } from '@nestjs/common'; 2 | 3 | import { PinoLogger, InjectPinoLogger } from '../src'; 4 | 5 | import { platforms } from './utils/platforms'; 6 | import { TestCase } from './utils/test-case'; 7 | 8 | describe('error logging', () => { 9 | for (const PlatformAdapter of platforms) { 10 | describe(PlatformAdapter.name, () => { 11 | describe('passing error directly', () => { 12 | it(InjectPinoLogger.name, async () => { 13 | const ctx = Math.random().toString(); 14 | 15 | @Controller('/') 16 | class TestController { 17 | constructor( 18 | @InjectPinoLogger(ctx) private readonly logger: PinoLogger, 19 | ) {} 20 | @Get() 21 | get() { 22 | this.logger.info(new Error('direct error passing')); 23 | return {}; 24 | } 25 | } 26 | 27 | const logs = await new TestCase(new PlatformAdapter(), { 28 | controllers: [TestController], 29 | }) 30 | .forRoot() 31 | .run(); 32 | expect( 33 | logs.some((v) => v.req && v.context === ctx && v.err), 34 | ).toBeTruthy(); 35 | }); 36 | 37 | it(PinoLogger.name, async () => { 38 | const ctx = Math.random().toString(); 39 | 40 | @Controller('/') 41 | class TestController { 42 | constructor(private readonly logger: PinoLogger) { 43 | this.logger.setContext(ctx); 44 | } 45 | 46 | @Get() 47 | get() { 48 | this.logger.info(new Error('direct error passing')); 49 | return {}; 50 | } 51 | } 52 | 53 | const logs = await new TestCase(new PlatformAdapter(), { 54 | controllers: [TestController], 55 | }) 56 | .forRoot() 57 | .run(); 58 | expect( 59 | logs.some((v) => v.req && v.context === ctx && v.err), 60 | ).toBeTruthy(); 61 | }); 62 | 63 | it(Logger.name, async () => { 64 | const ctx = Math.random().toString(); 65 | 66 | @Controller('/') 67 | class TestController { 68 | private readonly logger = new Logger(ctx); 69 | @Get() 70 | get() { 71 | this.logger.log(new Error('direct error passing')); 72 | return {}; 73 | } 74 | } 75 | 76 | const logs = await new TestCase(new PlatformAdapter(), { 77 | controllers: [TestController], 78 | }) 79 | .forRoot() 80 | .run(); 81 | expect( 82 | logs.some((v) => v.req && v.context === ctx && v.err), 83 | ).toBeTruthy(); 84 | }); 85 | }); 86 | 87 | describe('passing error with `err` field', () => { 88 | it(InjectPinoLogger.name, async () => { 89 | const ctx = Math.random().toString(); 90 | 91 | @Controller('/') 92 | class TestController { 93 | constructor( 94 | @InjectPinoLogger(ctx) private readonly logger: PinoLogger, 95 | ) {} 96 | @Get() 97 | get() { 98 | this.logger.info( 99 | { err: new Error('pino-style error passing'), foo: 'bar' }, 100 | 'baz', 101 | ); 102 | return {}; 103 | } 104 | } 105 | 106 | const logs = await new TestCase(new PlatformAdapter(), { 107 | controllers: [TestController], 108 | }) 109 | .forRoot() 110 | .run(); 111 | expect( 112 | logs.some( 113 | (v) => v.req && v.context === ctx && v.err && v.foo === 'bar', 114 | ), 115 | ).toBeTruthy(); 116 | }); 117 | 118 | it(PinoLogger.name, async () => { 119 | const ctx = Math.random().toString(); 120 | 121 | @Controller('/') 122 | class TestController { 123 | constructor(private readonly logger: PinoLogger) { 124 | this.logger.setContext(ctx); 125 | } 126 | 127 | @Get() 128 | get() { 129 | this.logger.info( 130 | { err: new Error('pino-style error passing'), foo: 'bar' }, 131 | 'baz', 132 | ); 133 | return {}; 134 | } 135 | } 136 | 137 | const logs = await new TestCase(new PlatformAdapter(), { 138 | controllers: [TestController], 139 | }) 140 | .forRoot() 141 | .run(); 142 | expect( 143 | logs.some( 144 | (v) => v.req && v.context === ctx && v.err && v.foo === 'bar', 145 | ), 146 | ).toBeTruthy(); 147 | }); 148 | 149 | it(Logger.name, async () => { 150 | const ctx = Math.random().toString(); 151 | 152 | @Controller('/') 153 | class TestController { 154 | private readonly logger = new Logger(ctx); 155 | @Get() 156 | get() { 157 | this.logger.log( 158 | { err: new Error('pino-style error passing'), foo: 'bar' }, 159 | 'baz', 160 | ); 161 | return {}; 162 | } 163 | } 164 | 165 | const logs = await new TestCase(new PlatformAdapter(), { 166 | controllers: [TestController], 167 | }) 168 | .forRoot() 169 | .run(); 170 | expect( 171 | logs.some( 172 | (v) => v.req && v.context === ctx && v.err && v.foo === 'bar', 173 | ), 174 | ).toBeTruthy(); 175 | }); 176 | }); 177 | 178 | describe('setting custom attribute keys', () => { 179 | it('setting the `err` custom attribute key', async () => { 180 | const ctx = Math.random().toString(); 181 | const message = 'custom `err` attribute key'; 182 | 183 | @Controller('/') 184 | class TestController { 185 | constructor(private readonly logger: PinoLogger) { 186 | this.logger.setContext(ctx); 187 | } 188 | 189 | @Get() 190 | get() { 191 | this.logger.info(new Error(message), 'baz'); 192 | return {}; 193 | } 194 | } 195 | 196 | const logs = await new TestCase(new PlatformAdapter(), { 197 | controllers: [TestController], 198 | }) 199 | .forRoot({ pinoHttp: { customAttributeKeys: { err: 'error' } } }) 200 | .run(); 201 | expect( 202 | logs.some( 203 | (v) => 204 | v.req && 205 | v.context === ctx && 206 | !v.err && 207 | v.error && 208 | (v.error as { message: string }).message === message, 209 | ), 210 | ).toBeTruthy(); 211 | }); 212 | 213 | it('setting the `req` custom attribute key', async () => { 214 | const ctx = Math.random().toString(); 215 | const message = 'custom `req` attribute key'; 216 | 217 | @Controller('/') 218 | class TestController { 219 | constructor(private readonly logger: PinoLogger) { 220 | this.logger.setContext(ctx); 221 | } 222 | 223 | @Get() 224 | get() { 225 | this.logger.info(new Error(message), 'baz'); 226 | return {}; 227 | } 228 | } 229 | 230 | const logs = await new TestCase(new PlatformAdapter(), { 231 | controllers: [TestController], 232 | }) 233 | .forRoot({ pinoHttp: { customAttributeKeys: { req: 'request' } } }) 234 | .run(); 235 | expect( 236 | logs.some( 237 | (v) => 238 | !v.req && 239 | v.request && 240 | v.context === ctx && 241 | v.err && 242 | v.err.message === message, 243 | ), 244 | ).toBeTruthy(); 245 | }); 246 | }); 247 | 248 | describe('keeps stack of thrown error', () => { 249 | it('built-in error handler logs with correct stack', async () => { 250 | const msg = Math.random().toString(); 251 | 252 | @Controller('/') 253 | class TestController { 254 | @Get() 255 | get() { 256 | throw new Error(msg); 257 | } 258 | } 259 | 260 | const logs = await new TestCase(new PlatformAdapter(), { 261 | controllers: [TestController], 262 | }) 263 | .forRoot() 264 | .expectError(500) 265 | .run(); 266 | 267 | expect( 268 | logs.some( 269 | (v) => 270 | v.req && 271 | v.msg === msg && 272 | v.err && 273 | v.err.message === msg && 274 | v.err.stack.includes(__filename) && 275 | v.err.stack.includes( 276 | `${TestController.name}.${TestController.prototype.get.name}`, 277 | ), 278 | ), 279 | ).toBeTruthy(); 280 | }); 281 | }); 282 | }); 283 | } 284 | }); 285 | -------------------------------------------------------------------------------- /__tests__/extending.spec.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Inject, Injectable } from '@nestjs/common'; 2 | 3 | import { Logger, Params, PARAMS_PROVIDER_TOKEN, PinoLogger } from '../src'; 4 | 5 | import { platforms } from './utils/platforms'; 6 | import { TestCase } from './utils/test-case'; 7 | 8 | describe('extending', () => { 9 | for (const PlatformAdapter of platforms) { 10 | describe(PlatformAdapter.name, () => { 11 | it('should work properly with an extended logger service', async () => { 12 | const msg = Math.random().toString(); 13 | 14 | @Injectable() 15 | class LoggerService extends Logger { 16 | private readonly message: string; 17 | constructor( 18 | logger: PinoLogger, 19 | @Inject(PARAMS_PROVIDER_TOKEN) params: Params, 20 | ) { 21 | super(logger, params); 22 | this.message = msg; 23 | } 24 | 25 | log() { 26 | this.logger.info(this.message); 27 | } 28 | } 29 | 30 | @Controller('/') 31 | class TestController { 32 | constructor(private readonly logger: LoggerService) {} 33 | @Get('/') 34 | get() { 35 | this.logger.log(); 36 | return {}; 37 | } 38 | } 39 | 40 | const logs = await new TestCase(new PlatformAdapter(), { 41 | providers: [LoggerService], 42 | exports: [LoggerService], 43 | controllers: [TestController], 44 | }) 45 | .forRoot() 46 | .run(); 47 | 48 | expect(logs.some((v) => v.msg === msg)).toBeTruthy(); 49 | }); 50 | 51 | it('should work properly with an extended PinoLogger service', async () => { 52 | const msg = Math.random().toString(); 53 | 54 | @Injectable() 55 | class LoggerService extends PinoLogger { 56 | private readonly message: string; 57 | constructor(@Inject(PARAMS_PROVIDER_TOKEN) params: Params) { 58 | super(params); 59 | this.message = msg; 60 | } 61 | 62 | log() { 63 | this.info(this.message); 64 | } 65 | } 66 | 67 | @Controller('/') 68 | class TestController { 69 | constructor(private readonly logger: LoggerService) {} 70 | @Get('/') 71 | get() { 72 | this.logger.log(); 73 | return {}; 74 | } 75 | } 76 | 77 | const logs = await new TestCase(new PlatformAdapter(), { 78 | providers: [LoggerService], 79 | exports: [LoggerService], 80 | controllers: [TestController], 81 | }) 82 | .forRoot() 83 | .run(); 84 | 85 | expect(logs.some((v) => v.msg === msg)).toBeTruthy(); 86 | }); 87 | }); 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /__tests__/get-logger-token.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | 3 | import { getLoggerToken, InjectPinoLogger, PinoLogger } from '../src'; 4 | 5 | describe('get-logger-token', () => { 6 | it('should work', async () => { 7 | class MyService { 8 | constructor( 9 | @InjectPinoLogger(MyService.name) private readonly logger: PinoLogger, 10 | ) {} 11 | } 12 | 13 | await Test.createTestingModule({ 14 | providers: [ 15 | MyService, 16 | { 17 | provide: getLoggerToken(MyService.name), 18 | useValue: {}, 19 | }, 20 | ], 21 | }).compile(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/levels.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Logger, 5 | ConsoleLogger, 6 | LogLevel, 7 | } from '@nestjs/common'; 8 | import pino from 'pino'; 9 | 10 | import { PinoLogger } from '../src'; 11 | 12 | import { platforms } from './utils/platforms'; 13 | import { TestCase } from './utils/test-case'; 14 | 15 | const loggerMethods: [LogLevel, pino.Level][] = [ 16 | ['verbose', 'trace'], 17 | ['debug', 'debug'], 18 | ['log', 'info'], 19 | ['warn', 'warn'], 20 | ['error', 'error'], 21 | ]; 22 | 23 | // the only way to make it work across different versions of nestjs 24 | if (ConsoleLogger.prototype.hasOwnProperty('fatal')) 25 | loggerMethods.push(['fatal', 'fatal']); 26 | 27 | const pinoLoggerMethods: pino.Level[] = loggerMethods 28 | .map((p) => p[1]) 29 | .concat('fatal'); 30 | 31 | describe(`Logger levels`, () => { 32 | for (const PlatformAdapter of platforms) { 33 | describe(PlatformAdapter.name, () => { 34 | for (const [loggerMethodName, pinoLevel] of loggerMethods) { 35 | it(loggerMethodName, async () => { 36 | const controllerMsg = Math.random().toString(); 37 | 38 | @Controller('/') 39 | class TestController { 40 | private readonly logger = new Logger(TestController.name); 41 | @Get() 42 | get() { 43 | this.logger[loggerMethodName](controllerMsg); 44 | return {}; 45 | } 46 | } 47 | 48 | const logs = await new TestCase(new PlatformAdapter(), { 49 | controllers: [TestController], 50 | }) 51 | .forRoot({ pinoHttp: { level: pinoLevel } }) 52 | .run(); 53 | 54 | expect(logs.some((v) => v.msg === controllerMsg)).toBeTruthy(); 55 | if ( 56 | pinoLevel === 'warn' || 57 | pinoLevel === 'error' || 58 | pinoLevel === 'fatal' 59 | ) { 60 | expect(logs.getStartLog()).toBeFalsy(); 61 | expect(logs.getResponseLog()).toBeFalsy(); 62 | } else { 63 | expect(logs.getStartLog()).toBeTruthy(); 64 | expect(logs.getResponseLog()).toBeTruthy(); 65 | } 66 | }); 67 | } 68 | }); 69 | } 70 | }); 71 | 72 | describe(`PinoLogger levels`, () => { 73 | for (const PlatformAdapter of platforms) { 74 | describe(PlatformAdapter.name, () => { 75 | // add fatal method 76 | for (const pinoLevel of pinoLoggerMethods) { 77 | it(pinoLevel, async () => { 78 | const controllerMsg = Math.random().toString(); 79 | 80 | @Controller('/') 81 | class TestController { 82 | constructor(private readonly logger: PinoLogger) {} 83 | @Get() 84 | get() { 85 | this.logger[pinoLevel](controllerMsg); 86 | return {}; 87 | } 88 | } 89 | 90 | const logs = await new TestCase(new PlatformAdapter(), { 91 | controllers: [TestController], 92 | }) 93 | .forRoot({ pinoHttp: { level: pinoLevel } }) 94 | .run(); 95 | 96 | expect(logs.some((v) => v.msg === controllerMsg)).toBeTruthy(); 97 | if ( 98 | pinoLevel === 'warn' || 99 | pinoLevel === 'error' || 100 | pinoLevel === 'fatal' 101 | ) { 102 | expect(logs.getStartLog()).toBeFalsy(); 103 | expect(logs.getResponseLog()).toBeFalsy(); 104 | } else { 105 | expect(logs.getStartLog()).toBeTruthy(); 106 | expect(logs.getResponseLog()).toBeTruthy(); 107 | } 108 | }); 109 | } 110 | }); 111 | } 112 | }); 113 | -------------------------------------------------------------------------------- /__tests__/logger-error-interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { APP_INTERCEPTOR } from '@nestjs/core'; 3 | 4 | import { LoggerErrorInterceptor } from '../src'; 5 | 6 | import { platforms } from './utils/platforms'; 7 | import { TestCase } from './utils/test-case'; 8 | 9 | describe('error intercepting', () => { 10 | for (const PlatformAdapter of platforms) { 11 | describe(PlatformAdapter.name, () => { 12 | it('logger is publicly accessible', async () => { 13 | class CustomError extends Error { 14 | constructor(message?: string) { 15 | super(message); 16 | Object.setPrototypeOf(this, new.target.prototype); 17 | Error.captureStackTrace(this, this.constructor); 18 | } 19 | } 20 | 21 | @Controller('/') 22 | class TestController { 23 | @Get() 24 | get() { 25 | throw new CustomError('Test Error Message'); 26 | } 27 | } 28 | 29 | const result = await new TestCase(new PlatformAdapter(), { 30 | controllers: [TestController], 31 | providers: [ 32 | { provide: APP_INTERCEPTOR, useClass: LoggerErrorInterceptor }, 33 | ], 34 | }) 35 | .forRoot() 36 | .expectError(500) 37 | .run(); 38 | 39 | expect( 40 | result.find((log) => log.msg === 'request errored'), 41 | ).toMatchObject({ 42 | err: { 43 | message: 'Test Error Message', 44 | type: 'CustomError', 45 | }, 46 | }); 47 | }); 48 | }); 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /__tests__/logger-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | 3 | import { PinoLogger } from '../src'; 4 | 5 | import { platforms } from './utils/platforms'; 6 | import { TestCase } from './utils/test-case'; 7 | 8 | describe('getting the logger instance', () => { 9 | for (const PlatformAdapter of platforms) { 10 | describe(PlatformAdapter.name, () => { 11 | it('logger is publicly accessible', async () => { 12 | @Controller('/') 13 | class TestController { 14 | constructor(private readonly logger: PinoLogger) {} 15 | @Get() 16 | get() { 17 | expect(this.logger.logger.constructor.name).toEqual('Pino'); 18 | } 19 | } 20 | 21 | await new TestCase(new PlatformAdapter(), { 22 | controllers: [TestController], 23 | }) 24 | .forRoot() 25 | .run(); 26 | }); 27 | }); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/module.spec.ts: -------------------------------------------------------------------------------- 1 | import { Module, Controller, Get, Injectable, Logger } from '@nestjs/common'; 2 | // eslint-disable-next-line @typescript-eslint/no-require-imports 3 | import MemoryStream = require('memorystream'); 4 | 5 | import { LoggerModule, Params } from '../src'; 6 | 7 | import { LogsContainer } from './utils/logs'; 8 | import { platforms } from './utils/platforms'; 9 | import { TestCase } from './utils/test-case'; 10 | 11 | describe('module initialization', () => { 12 | for (const PlatformAdapter of platforms) { 13 | describe(PlatformAdapter.name, () => { 14 | describe('forRoot', () => { 15 | it('should work properly without params', async () => { 16 | @Controller('/') 17 | class TestController { 18 | private readonly logger = new Logger(TestController.name); 19 | 20 | @Get('/') 21 | get() { 22 | this.logger.log(''); 23 | return {}; 24 | } 25 | } 26 | 27 | await new TestCase(new PlatformAdapter(), { 28 | imports: [LoggerModule.forRoot()], 29 | controllers: [TestController], 30 | }) 31 | .forRoot(undefined, true) 32 | .run(); 33 | }); 34 | 35 | it('should work properly with single value of `httpPino` property', async () => { 36 | const msg = Math.random().toString(); 37 | 38 | @Controller('/') 39 | class TestController { 40 | private readonly logger = new Logger(TestController.name); 41 | @Get('/') 42 | get() { 43 | this.logger.log(msg); 44 | return {}; 45 | } 46 | } 47 | 48 | const logs = await new TestCase(new PlatformAdapter(), { 49 | controllers: [TestController], 50 | }) 51 | .forRoot({ pinoHttp: { level: 'info' } }) 52 | .run(); 53 | 54 | expect(logs.some((v) => v.msg === msg)).toBeTruthy(); 55 | }); 56 | 57 | it('should work properly with array as value of `httpPino` property', async () => { 58 | const stream = new MemoryStream('', { readable: false }); 59 | const msg = Math.random().toString(); 60 | 61 | @Controller('/') 62 | class TestController { 63 | private readonly logger = new Logger(TestController.name); 64 | @Get('/') 65 | get() { 66 | this.logger.log(msg); 67 | return {}; 68 | } 69 | } 70 | 71 | await new TestCase(new PlatformAdapter(), { 72 | controllers: [TestController], 73 | }) 74 | .forRoot({ pinoHttp: [{ level: 'info' }, stream] }, true) 75 | .run(); 76 | 77 | const logs = LogsContainer.from(stream); 78 | expect(logs.some((v) => v.msg === msg)).toBeTruthy(); 79 | }); 80 | }); 81 | 82 | describe('forRootAsync', () => { 83 | it('should work properly when useFactory returns single value of `httpPino` property', async () => { 84 | const msg = Math.random().toString(); 85 | 86 | @Controller('/') 87 | class TestController { 88 | private readonly logger = new Logger(TestController.name); 89 | @Get('/') 90 | get() { 91 | this.logger.log(msg); 92 | return {}; 93 | } 94 | } 95 | 96 | @Injectable() 97 | class Config { 98 | readonly level = 'info'; 99 | } 100 | 101 | @Module({ 102 | providers: [Config], 103 | exports: [Config], 104 | }) 105 | class ConfigModule {} 106 | 107 | const logs = await new TestCase(new PlatformAdapter(), { 108 | controllers: [TestController], 109 | }) 110 | .forRootAsync({ 111 | imports: [ConfigModule], 112 | inject: [Config], 113 | useFactory: (cfg: Config) => { 114 | return { pinoHttp: { level: cfg.level } }; 115 | }, 116 | }) 117 | .run(); 118 | 119 | expect(logs.some((v) => v.msg === msg)).toBeTruthy(); 120 | }); 121 | 122 | it('should work properly when useFactory returns array as value of `httpPino` property', async () => { 123 | const stream = new MemoryStream('', { readable: false }); 124 | const msg = Math.random().toString(); 125 | 126 | @Controller('/') 127 | class TestController { 128 | private readonly logger = new Logger(TestController.name); 129 | @Get('/') 130 | get() { 131 | this.logger.log(msg); 132 | return {}; 133 | } 134 | } 135 | 136 | @Injectable() 137 | class Config { 138 | readonly level = 'info'; 139 | } 140 | 141 | @Module({ 142 | providers: [Config], 143 | exports: [Config], 144 | }) 145 | class ConfigModule {} 146 | 147 | await new TestCase(new PlatformAdapter(), { 148 | controllers: [TestController], 149 | }) 150 | .forRootAsync( 151 | { 152 | imports: [ConfigModule], 153 | inject: [Config], 154 | useFactory: (cfg: Config) => { 155 | return { pinoHttp: [{ level: cfg.level }, stream] }; 156 | }, 157 | }, 158 | true, 159 | ) 160 | .run(); 161 | 162 | const logs = LogsContainer.from(stream); 163 | expect(logs.some((v) => v.msg === msg)).toBeTruthy(); 164 | }); 165 | 166 | it('should work properly when pass deps via providers', async () => { 167 | const msg = Math.random().toString(); 168 | 169 | @Controller('/') 170 | class TestController { 171 | private readonly logger = new Logger(TestController.name); 172 | @Get('/') 173 | get() { 174 | this.logger.log(msg); 175 | return {}; 176 | } 177 | } 178 | 179 | @Injectable() 180 | class Config { 181 | readonly level = 'info'; 182 | } 183 | 184 | const logs = await new TestCase(new PlatformAdapter(), { 185 | controllers: [TestController], 186 | }) 187 | .forRootAsync({ 188 | providers: [Config], 189 | inject: [Config], 190 | useFactory: (cfg: Config) => { 191 | return { pinoHttp: { level: cfg.level } }; 192 | }, 193 | }) 194 | .run(); 195 | 196 | expect(logs.some((v) => v.msg === msg)).toBeTruthy(); 197 | }); 198 | 199 | it('should work properly when useFactory returns Promise', async () => { 200 | const msg = Math.random().toString(); 201 | 202 | @Controller('/') 203 | class TestController { 204 | private readonly logger = new Logger(TestController.name); 205 | @Get('/') 206 | get() { 207 | this.logger.log(msg); 208 | return {}; 209 | } 210 | } 211 | 212 | const logs = await new TestCase(new PlatformAdapter(), { 213 | controllers: [TestController], 214 | }) 215 | .forRootAsync({ 216 | useFactory: async (): Promise => { 217 | return { pinoHttp: { level: 'info' } }; 218 | }, 219 | }) 220 | .run(); 221 | 222 | expect(logs.some((v) => v.msg === msg)).toBeTruthy(); 223 | }); 224 | }); 225 | }); 226 | } 227 | }); 228 | -------------------------------------------------------------------------------- /__tests__/no-context.spec.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Logger } from '@nestjs/common'; 2 | 3 | import { InjectPinoLogger, PinoLogger } from '../src'; 4 | 5 | import { platforms } from './utils/platforms'; 6 | import { TestCase } from './utils/test-case'; 7 | 8 | describe('no context', () => { 9 | for (const PlatformAdapter of platforms) { 10 | describe(PlatformAdapter.name, () => { 11 | it(Logger.name, async () => { 12 | const msg = Math.random().toString(); 13 | 14 | @Controller('/') 15 | class TestController { 16 | private readonly logger = new Logger(); 17 | 18 | @Get() 19 | get() { 20 | this.logger.log(msg); 21 | return {}; 22 | } 23 | } 24 | 25 | const logs = await new TestCase(new PlatformAdapter(), { 26 | controllers: [TestController], 27 | }) 28 | .forRoot() 29 | .run(); 30 | 31 | const ctrlLog = logs.find((v) => v.msg === msg); 32 | expect(ctrlLog).toBeTruthy(); 33 | expect(ctrlLog).not.toHaveProperty('context'); 34 | }); 35 | 36 | it(PinoLogger.name, async () => { 37 | const msg = Math.random().toString(); 38 | 39 | @Controller('/') 40 | class TestController { 41 | constructor( 42 | @InjectPinoLogger() private readonly logger: PinoLogger, 43 | ) {} 44 | 45 | @Get() 46 | get() { 47 | this.logger.info(msg); 48 | return {}; 49 | } 50 | } 51 | 52 | const logs = await new TestCase(new PlatformAdapter(), { 53 | controllers: [TestController], 54 | }) 55 | .forRoot() 56 | .run(); 57 | 58 | const ctrlLog = logs.find((v) => v.msg === msg); 59 | expect(ctrlLog).toBeTruthy(); 60 | expect(ctrlLog).not.toHaveProperty('context'); 61 | }); 62 | }); 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /__tests__/rename-context.spec.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Logger } from '@nestjs/common'; 2 | 3 | import { PinoLogger, InjectPinoLogger } from '../src'; 4 | 5 | import { platforms } from './utils/platforms'; 6 | import { TestCase } from './utils/test-case'; 7 | 8 | describe('rename context property name', () => { 9 | for (const PlatformAdapter of platforms) { 10 | describe(PlatformAdapter.name, () => { 11 | it(InjectPinoLogger.name, async () => { 12 | const ctxFiledName = 'ctx'; 13 | const msg = Math.random().toString(); 14 | 15 | @Controller('/') 16 | class TestController { 17 | constructor( 18 | @InjectPinoLogger(TestController.name) 19 | private readonly logger: PinoLogger, 20 | ) {} 21 | @Get() 22 | get() { 23 | this.logger.info(msg); 24 | return {}; 25 | } 26 | } 27 | 28 | const logs = await new TestCase(new PlatformAdapter(), { 29 | controllers: [TestController], 30 | }) 31 | .forRoot({ renameContext: ctxFiledName }) 32 | .run(); 33 | expect( 34 | logs.some( 35 | (v) => 36 | v.req && v[ctxFiledName] === TestController.name && v.msg === msg, 37 | ), 38 | ).toBeTruthy(); 39 | expect(logs.getStartLog()).toHaveProperty(ctxFiledName); 40 | }); 41 | 42 | it(PinoLogger.name, async () => { 43 | const ctxFiledName = 'ctx'; 44 | const msg = Math.random().toString(); 45 | 46 | @Controller('/') 47 | class TestController { 48 | constructor(private readonly logger: PinoLogger) { 49 | this.logger.setContext(TestController.name); 50 | } 51 | 52 | @Get() 53 | get() { 54 | this.logger.info(msg); 55 | return {}; 56 | } 57 | } 58 | 59 | const logs = await new TestCase(new PlatformAdapter(), { 60 | controllers: [TestController], 61 | }) 62 | .forRoot({ renameContext: ctxFiledName }) 63 | .run(); 64 | expect( 65 | logs.some( 66 | (v) => 67 | v.req && v[ctxFiledName] === TestController.name && v.msg === msg, 68 | ), 69 | ).toBeTruthy(); 70 | expect(logs.getStartLog()).toHaveProperty(ctxFiledName); 71 | }); 72 | 73 | it(Logger.name, async () => { 74 | const ctxFiledName = 'ctx'; 75 | const msg = Math.random().toString(); 76 | 77 | @Controller('/') 78 | class TestController { 79 | private readonly logger = new Logger(TestController.name); 80 | @Get() 81 | get() { 82 | this.logger.log(msg); 83 | return {}; 84 | } 85 | } 86 | 87 | const logs = await new TestCase(new PlatformAdapter(), { 88 | controllers: [TestController], 89 | }) 90 | .forRoot({ renameContext: ctxFiledName }) 91 | .run(); 92 | expect( 93 | logs.some( 94 | (v) => 95 | v.req && v[ctxFiledName] === TestController.name && v.msg === msg, 96 | ), 97 | ).toBeTruthy(); 98 | expect(logs.getStartLog()).toHaveProperty(ctxFiledName); 99 | }); 100 | }); 101 | } 102 | }); 103 | -------------------------------------------------------------------------------- /__tests__/routing.spec.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Logger, RequestMethod } from '@nestjs/common'; 2 | 3 | import { platforms } from './utils/platforms'; 4 | import { TestCase } from './utils/test-case'; 5 | 6 | describe('routing', () => { 7 | for (const PlatformAdapter of platforms) { 8 | describe(PlatformAdapter.name, () => { 9 | let includedMsg: string; 10 | let excludedMsg: string; 11 | let notIncludedMsg: string; 12 | let testCase: TestCase; 13 | 14 | beforeEach(async () => { 15 | includedMsg = Math.random().toString(); 16 | excludedMsg = Math.random().toString(); 17 | notIncludedMsg = Math.random().toString(); 18 | 19 | @Controller('/') 20 | class LoggingController { 21 | private readonly logger = new Logger(LoggingController.name); 22 | 23 | @Get('/include') 24 | withLog() { 25 | this.logger.log(includedMsg); 26 | return {}; 27 | } 28 | 29 | @Get('/exclude') 30 | skipLog() { 31 | this.logger.log(excludedMsg); 32 | return {}; 33 | } 34 | } 35 | 36 | @Controller('/not-include') 37 | class NoLoggingController { 38 | private readonly logger = new Logger(NoLoggingController.name); 39 | 40 | @Get() 41 | get() { 42 | this.logger.log(notIncludedMsg); 43 | return {}; 44 | } 45 | } 46 | 47 | testCase = new TestCase(new PlatformAdapter(), { 48 | controllers: [LoggingController, NoLoggingController], 49 | }).forRoot({ 50 | forRoutes: [LoggingController], 51 | exclude: [{ method: RequestMethod.GET, path: '/exclude' }], 52 | }); 53 | }); 54 | 55 | it('included', async () => { 56 | const logs = await testCase.run('/include'); 57 | expect(logs.some((v) => v.msg === includedMsg && !!v.req)).toBeTruthy(); 58 | expect(logs.getResponseLog()).toBeTruthy(); 59 | }); 60 | 61 | it('excluded', async () => { 62 | const logs = await testCase.run('/exclude'); 63 | expect(logs.some((v) => v.msg === excludedMsg && !v.req)).toBeTruthy(); 64 | expect(logs.getResponseLog()).toBeFalsy(); 65 | }); 66 | 67 | it('not included', async () => { 68 | const logs = await testCase.run('/not-include'); 69 | expect( 70 | logs.some((v) => v.msg === notIncludedMsg && !v.req), 71 | ).toBeTruthy(); 72 | expect(logs.getResponseLog()).toBeFalsy(); 73 | }); 74 | }); 75 | } 76 | }); 77 | -------------------------------------------------------------------------------- /__tests__/runtime-update.spec.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { FastifyAdapter } from '@nestjs/platform-fastify'; 3 | 4 | import { PinoLogger } from '../src'; 5 | 6 | import { platforms } from './utils/platforms'; 7 | import { TestCase } from './utils/test-case'; 8 | 9 | describe('runtime update', () => { 10 | for (const PlatformAdapter of platforms) { 11 | describe(PlatformAdapter.name, () => { 12 | it('works', async () => { 13 | const msg1 = Math.random().toString(); 14 | const msg2 = Math.random().toString(); 15 | const messages = [msg1, msg2]; 16 | let msgIdx = 0; 17 | 18 | const pathCheck = '/log-with-level-info'; 19 | const pathSetLevel = '/set-level-info'; 20 | 21 | @Controller('/') 22 | class TestController { 23 | constructor(private readonly logger: PinoLogger) {} 24 | 25 | @Get(pathCheck) 26 | check() { 27 | this.logger.info(messages[msgIdx++]); 28 | return {}; 29 | } 30 | 31 | @Get(pathSetLevel) 32 | setLevel() { 33 | PinoLogger.root.level = 'info'; 34 | return {}; 35 | } 36 | } 37 | 38 | const logs = await new TestCase(new PlatformAdapter(), { 39 | controllers: [TestController], 40 | }) 41 | .forRoot({ pinoHttp: { level: 'silent' } }) 42 | .run(pathCheck, pathSetLevel, pathCheck); 43 | 44 | expect(logs.some((l) => l.msg === msg1)).toBeFalsy(); 45 | expect(logs.some((l) => l.msg === msg2)).toBeTruthy(); 46 | }); 47 | }); 48 | } 49 | 50 | it("doesn't work with useExisting", async () => { 51 | @Controller('/') 52 | class TestController { 53 | @Get() 54 | setLevel() { 55 | expect(PinoLogger.root).toBeUndefined(); 56 | return {}; 57 | } 58 | } 59 | 60 | await new TestCase(new FastifyAdapter(), { 61 | controllers: [TestController], 62 | }) 63 | .forRoot({ useExisting: true }) 64 | .run(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /__tests__/use-existing.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Injectable, 5 | OnModuleInit, 6 | Logger, 7 | } from '@nestjs/common'; 8 | import { FastifyAdapter } from '@nestjs/platform-fastify'; 9 | // eslint-disable-next-line @typescript-eslint/no-require-imports 10 | import MemoryStream = require('memorystream'); 11 | import pino from 'pino'; 12 | 13 | import { LogsContainer } from './utils/logs'; 14 | import { platforms } from './utils/platforms'; 15 | import { TestCase } from './utils/test-case'; 16 | 17 | describe('useExisting property', () => { 18 | describe(FastifyAdapter.name, () => { 19 | it('should use adapter logger in req context and default beyond', async () => { 20 | const stream = new MemoryStream('', { readable: false }); 21 | const inReqContextMsg = Math.random().toString(); 22 | const outReqContextMsg = Math.random().toString(); 23 | 24 | @Injectable() 25 | class TestService implements OnModuleInit { 26 | private readonly logger = new Logger(TestService.name); 27 | 28 | someMethod() { 29 | this.logger.log(inReqContextMsg); 30 | } 31 | onModuleInit() { 32 | this.logger.log(outReqContextMsg); 33 | } 34 | } 35 | 36 | @Controller('/') 37 | class TestController { 38 | constructor(private readonly service: TestService) {} 39 | @Get('/') 40 | get() { 41 | this.service.someMethod(); 42 | return {}; 43 | } 44 | } 45 | 46 | await new TestCase(new FastifyAdapter({ logger: { stream } }), { 47 | controllers: [TestController], 48 | providers: [TestService], 49 | }) 50 | .forRoot({ useExisting: true }) 51 | .run(); 52 | 53 | // In this case we are checking custom stream, that was passed to pino 54 | // via FastifyAdapter. 55 | const logs = LogsContainer.from(stream); 56 | 57 | // existing stream is used for logs in request context 58 | expect(logs.some((v) => v.msg === inReqContextMsg)).toBeTruthy(); 59 | // out of context log will be sent to stdout because of standard config 60 | expect(logs.some((v) => v.msg === outReqContextMsg)).toBeFalsy(); 61 | }); 62 | }); 63 | }); 64 | 65 | describe('pass existing pino instance', () => { 66 | for (const PlatformAdapter of platforms) { 67 | describe(PlatformAdapter.name, () => { 68 | it('should use passed instance out of context', async () => { 69 | const stream = new MemoryStream('', { readable: false }); 70 | const msg = Math.random().toString(); 71 | 72 | @Injectable() 73 | class TestService implements OnModuleInit { 74 | private readonly logger = new Logger(TestService.name); 75 | onModuleInit() { 76 | this.logger.log(msg); 77 | } 78 | } 79 | 80 | @Controller('/') 81 | class TestController { 82 | @Get('/') 83 | get() { 84 | return {}; 85 | } 86 | } 87 | 88 | const instance = pino(stream); 89 | 90 | await new TestCase(new PlatformAdapter(), { 91 | controllers: [TestController], 92 | providers: [TestService], 93 | }) 94 | .forRoot({ pinoHttp: { logger: instance } }, true) 95 | .run(); 96 | 97 | const logs = LogsContainer.from(stream); 98 | expect(logs.some((v) => v.msg === msg)).toBeTruthy(); 99 | }); 100 | }); 101 | } 102 | }); 103 | -------------------------------------------------------------------------------- /__tests__/utils/get-free-port.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'net'; 2 | 3 | export async function getFreePort() { 4 | return new Promise((res) => { 5 | const srv = createServer(); 6 | srv.listen(0, () => { 7 | const address = srv.address(); 8 | assertPortField(address); 9 | srv.close(() => res(address.port)); 10 | }); 11 | }); 12 | } 13 | 14 | function assertPortField(x: unknown): asserts x is { port: number } { 15 | expect(x).toMatchObject({ port: expect.any(Number) }); 16 | } 17 | -------------------------------------------------------------------------------- /__tests__/utils/logs.ts: -------------------------------------------------------------------------------- 1 | const startMsg = 'Nest application successfully started'; 2 | const responseMsg = 'request completed'; 3 | 4 | export type LogObject = { 5 | msg: string; 6 | req?: { id: number }; 7 | res?: Record; 8 | context?: string; 9 | err?: { message: string; stack: string; type: string }; 10 | [key: string]: unknown; 11 | }; 12 | 13 | export class LogsContainer { 14 | static from(stringer: { toString(): string }) { 15 | return new LogsContainer(stringer.toString()); 16 | } 17 | 18 | private readonly logs: LogObject[]; 19 | 20 | constructor(logs: string) { 21 | this.logs = logs 22 | .split('\n') 23 | .map((v) => v.trim()) 24 | .filter((v) => !!v) 25 | .map((v) => JSON.parse(v)); 26 | } 27 | 28 | get some() { 29 | return this.logs.some.bind(this.logs); 30 | } 31 | 32 | get find() { 33 | return this.logs.find.bind(this.logs); 34 | } 35 | 36 | getStartLog(): LogObject | undefined { 37 | return this.logs.find((log) => log.msg.startsWith(startMsg)); 38 | } 39 | 40 | getResponseLog(): LogObject | undefined { 41 | return this.logs.find((log) => log.msg === responseMsg); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /__tests__/utils/platforms.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { AbstractHttpAdapter } from '@nestjs/core'; 3 | import { ExpressAdapter } from '@nestjs/platform-express'; 4 | import { FastifyAdapter } from '@nestjs/platform-fastify'; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | export type Adapter = Type>; 8 | 9 | export const platforms: Adapter[] = [ExpressAdapter, FastifyAdapter]; 10 | -------------------------------------------------------------------------------- /__tests__/utils/test-case.ts: -------------------------------------------------------------------------------- 1 | import { Module, ModuleMetadata, Type } from '@nestjs/common'; 2 | import { AbstractHttpAdapter, NestFactory } from '@nestjs/core'; 3 | // eslint-disable-next-line @typescript-eslint/no-require-imports 4 | import MemoryStream = require('memorystream'); 5 | import pino from 'pino'; 6 | import { Options } from 'pino-http'; 7 | import * as request from 'supertest'; 8 | 9 | import { 10 | Logger, 11 | LoggerModule, 12 | LoggerModuleAsyncParams, 13 | Params, 14 | } from '../../src'; 15 | import { __resetOutOfContextForTests as __resetSingletons } from '../../src/PinoLogger'; 16 | 17 | import { getFreePort } from './get-free-port'; 18 | import { LogsContainer } from './logs'; 19 | 20 | export class TestCase { 21 | private module?: Type; 22 | private stream: pino.DestinationStream; 23 | private expectedCode = 200; 24 | 25 | constructor( 26 | private readonly adapter: AbstractHttpAdapter, 27 | private readonly moduleMetadata: ModuleMetadata, 28 | ) { 29 | this.stream = new MemoryStream('', { readable: false }); 30 | } 31 | 32 | forRoot(params?: Params | undefined, skipStreamInjection = false): this { 33 | let finalParams: Params | undefined = params; 34 | 35 | if (!skipStreamInjection) { 36 | finalParams = this.injectStream(params); 37 | } 38 | 39 | @Module({ 40 | ...this.moduleMetadata, 41 | imports: [ 42 | LoggerModule.forRoot(finalParams), 43 | ...(this.moduleMetadata.imports || []), 44 | ], 45 | }) 46 | class TestModule {} 47 | this.module = TestModule; 48 | 49 | return this; 50 | } 51 | 52 | forRootAsync( 53 | asyncParams: LoggerModuleAsyncParams, 54 | skipStreamInjection = false, 55 | ): this { 56 | if (!skipStreamInjection) { 57 | const useFactoryOld = asyncParams.useFactory; 58 | asyncParams.useFactory = (...args: unknown[]) => { 59 | const params = useFactoryOld(...args); 60 | if ('then' in params) { 61 | return params.then((p) => this.injectStream(p)); 62 | } 63 | return this.injectStream(params); 64 | }; 65 | } 66 | 67 | @Module({ 68 | ...this.moduleMetadata, 69 | imports: [ 70 | ...(this.moduleMetadata.imports || []), 71 | LoggerModule.forRootAsync(asyncParams), 72 | ], 73 | }) 74 | class TestModule {} 75 | this.module = TestModule; 76 | 77 | return this; 78 | } 79 | 80 | expectError(code: number): this { 81 | this.expectedCode = code; 82 | return this; 83 | } 84 | 85 | async run(...paths: string[]): Promise { 86 | if (paths.length === 0) { 87 | paths = ['/']; 88 | } 89 | expect(this.module).toBeTruthy(); 90 | if (!this.module) throw new Error(); 91 | 92 | __resetSingletons(); 93 | 94 | const app = await NestFactory.create(this.module, this.adapter, { 95 | bufferLogs: true, 96 | }); 97 | app.useLogger(app.get(Logger)); 98 | 99 | const server = await app.listen(await getFreePort(), '0.0.0.0'); 100 | for (const path of paths) { 101 | await request(server).get(path).expect(this.expectedCode); 102 | } 103 | await app.close(); 104 | 105 | return LogsContainer.from(this.stream); 106 | } 107 | 108 | private injectStream(params: Params | undefined): Params { 109 | switch (true) { 110 | case !params: 111 | return { pinoHttp: this.stream }; 112 | case !!params!.useExisting: 113 | return params!; 114 | case Array.isArray(params!.pinoHttp): 115 | return { 116 | ...params, 117 | pinoHttp: [ 118 | (params!.pinoHttp as [Options, pino.DestinationStream])[0], 119 | this.stream, 120 | ], 121 | }; 122 | case !!params!.pinoHttp: 123 | return { 124 | ...params, 125 | pinoHttp: { 126 | ...(params!.pinoHttp as Options), 127 | stream: this.stream, 128 | }, 129 | }; 130 | default: 131 | return { 132 | ...params, 133 | pinoHttp: this.stream, 134 | }; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /eslint.config.cjs: -------------------------------------------------------------------------------- 1 | const { fixupConfigRules, fixupPluginRules } = require('@eslint/compat'); 2 | const { FlatCompat } = require('@eslint/eslintrc'); 3 | const js = require('@eslint/js'); 4 | const typescriptEslintEslintPlugin = require('@typescript-eslint/eslint-plugin'); 5 | const tsParser = require('@typescript-eslint/parser'); 6 | const globals = require('globals'); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | recommendedConfig: js.configs.recommended, 11 | allConfig: js.configs.all, 12 | }); 13 | 14 | module.exports = [ 15 | { 16 | ignores: ['eslint.config.cjs'], 17 | }, 18 | ...fixupConfigRules( 19 | compat.extends( 20 | 'plugin:@typescript-eslint/recommended', 21 | 'plugin:import/recommended', 22 | 'plugin:import/typescript', 23 | 'plugin:prettier/recommended', 24 | ), 25 | ), 26 | { 27 | plugins: { 28 | '@typescript-eslint': fixupPluginRules(typescriptEslintEslintPlugin), 29 | }, 30 | 31 | languageOptions: { 32 | globals: { 33 | ...globals.node, 34 | ...globals.jest, 35 | }, 36 | 37 | parser: tsParser, 38 | ecmaVersion: 5, 39 | sourceType: 'module', 40 | 41 | parserOptions: { 42 | project: 'tsconfig.json', 43 | }, 44 | }, 45 | 46 | rules: { 47 | '@typescript-eslint/no-explicit-any': [ 48 | 'error', 49 | { 50 | fixToUnknown: true, 51 | ignoreRestArgs: false, 52 | }, 53 | ], 54 | 55 | '@typescript-eslint/no-floating-promises': [ 56 | 'error', 57 | { 58 | ignoreIIFE: true, 59 | }, 60 | ], 61 | 62 | '@typescript-eslint/no-unused-vars': [ 63 | 'error', 64 | { 65 | varsIgnorePattern: '^_', 66 | argsIgnorePattern: '^_', 67 | ignoreRestSiblings: true, 68 | }, 69 | ], 70 | 71 | '@typescript-eslint/explicit-member-accessibility': [ 72 | 'error', 73 | { 74 | accessibility: 'no-public', 75 | }, 76 | ], 77 | 78 | '@typescript-eslint/member-ordering': [ 79 | 'error', 80 | { 81 | default: [ 82 | 'public-static-field', 83 | 'public-static-get', 84 | 'public-static-set', 85 | 'public-static-method', 86 | 'protected-static-field', 87 | 'protected-static-get', 88 | 'protected-static-set', 89 | 'protected-static-method', 90 | 'private-static-field', 91 | 'private-static-get', 92 | 'private-static-set', 93 | 'private-static-method', 94 | 'signature', 95 | 'public-abstract-field', 96 | 'protected-abstract-field', 97 | 'public-decorated-field', 98 | 'public-instance-field', 99 | 'protected-decorated-field', 100 | 'protected-instance-field', 101 | 'private-decorated-field', 102 | 'private-instance-field', 103 | 'public-constructor', 104 | 'protected-constructor', 105 | 'private-constructor', 106 | 'public-abstract-get', 107 | 'public-abstract-set', 108 | 'public-abstract-method', 109 | 'public-decorated-get', 110 | 'public-instance-get', 111 | 'public-decorated-set', 112 | 'public-instance-set', 113 | 'public-decorated-method', 114 | 'public-instance-method', 115 | 'protected-abstract-get', 116 | 'protected-abstract-set', 117 | 'protected-abstract-method', 118 | 'protected-decorated-get', 119 | 'protected-instance-get', 120 | 'protected-decorated-set', 121 | 'protected-instance-set', 122 | 'protected-decorated-method', 123 | 'protected-instance-method', 124 | 'private-decorated-get', 125 | 'private-instance-get', 126 | 'private-decorated-set', 127 | 'private-instance-set', 128 | 'private-decorated-method', 129 | 'private-instance-method', 130 | ], 131 | }, 132 | ], 133 | 134 | 'import/order': [ 135 | 'error', 136 | { 137 | alphabetize: { 138 | order: 'asc', 139 | caseInsensitive: true, 140 | }, 141 | 142 | 'newlines-between': 'always', 143 | }, 144 | ], 145 | }, 146 | }, 147 | ]; 148 | -------------------------------------------------------------------------------- /example/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Logger } from '@nestjs/common'; 2 | 3 | import { InjectPinoLogger, PinoLogger } from '../src'; 4 | 5 | import { MyService } from './my.service'; 6 | 7 | @Controller() 8 | export class AppController { 9 | // Choose which one do you like more: usage of builtin logger that implements 10 | // `LoggerService` (verbose?,debug?,log,warn,error) or ... 11 | private readonly builtInLogger = new Logger(AppController.name); 12 | 13 | constructor( 14 | private readonly myService: MyService, 15 | // ... logger that implements `pino` (trace,debug,info,warn,error,fatal). 16 | // For the last one you can choose which of this two injection methods do 17 | // you like more: 18 | @InjectPinoLogger(AppController.name) 19 | private readonly pinoLogger1: PinoLogger, 20 | private readonly pinoLogger2: PinoLogger, 21 | ) { 22 | pinoLogger2.setContext(AppController.name); 23 | } 24 | 25 | @Get() 26 | getHello(): string { 27 | this.builtInLogger.verbose({ foo: 'bar' }, 'baz %s', 'qux'); // will be skipped because debug level is set 28 | this.builtInLogger.debug({ foo: 'bar' }, 'baz %s', 'qux'); 29 | this.builtInLogger.log({ foo: 'bar' }, 'baz %s', 'qux'); 30 | this.builtInLogger.warn({ foo: 'bar' }, 'baz %s', 'qux'); 31 | this.builtInLogger.error({ foo: 'bar' }, 'baz %s', 'qux'); 32 | 33 | this.pinoLogger1.trace({ foo: 'bar' }, 'baz %s', 'qux'); // will be skipped because debug level is set 34 | this.pinoLogger1.debug({ foo: 'bar' }, 'baz %s', 'qux'); 35 | this.pinoLogger1.info({ foo: 'bar' }, 'baz %s', 'qux'); 36 | this.pinoLogger1.warn({ foo: 'bar' }, 'baz %s', 'qux'); 37 | this.pinoLogger1.error({ foo: 'bar' }, 'baz %s', 'qux'); 38 | this.pinoLogger1.fatal({ foo: 'bar' }, 'baz %s', 'qux'); 39 | 40 | this.pinoLogger2.trace({ foo: 'bar' }, 'baz %s', 'qux'); // will be skipped because debug level is set 41 | this.pinoLogger2.debug({ foo: 'bar' }, 'baz %s', 'qux'); 42 | this.pinoLogger2.info({ foo: 'bar' }, 'baz %s', 'qux'); 43 | this.pinoLogger2.warn({ foo: 'bar' }, 'baz %s', 'qux'); 44 | this.pinoLogger2.error({ foo: 'bar' }, 'baz %s', 'qux'); 45 | this.pinoLogger2.fatal({ foo: 'bar' }, 'baz %s', 'qux'); 46 | 47 | return `Hello ${this.myService.getWorld()}`; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { LoggerModule } from '../src'; 4 | 5 | import { AppController } from './app.controller'; 6 | import { MyService } from './my.service'; 7 | 8 | @Module({ 9 | imports: [ 10 | LoggerModule.forRoot({ pinoHttp: { level: process.env.LOG_LEVEL } }), 11 | ], 12 | controllers: [AppController], 13 | providers: [MyService], 14 | }) 15 | export class AppModule {} 16 | -------------------------------------------------------------------------------- /example/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | 3 | import { Logger } from '../src'; 4 | 5 | import { AppModule } from './app.module'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(AppModule, { bufferLogs: true }); 9 | app.useLogger(app.get(Logger)); 10 | await app.listen(3000); 11 | } 12 | 13 | bootstrap().catch(console.error); 14 | -------------------------------------------------------------------------------- /example/my.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class MyService { 5 | getWorld() { 6 | return 'World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'ts'], 3 | testRegex: '.spec.ts$', 4 | transform: { 5 | '^.+\\.(t|j)s$': 'ts-jest', 6 | }, 7 | collectCoverage: true, 8 | coverageDirectory: './coverage', 9 | collectCoverageFrom: ['src/**/*.ts'], 10 | testEnvironment: 'node', 11 | setupFiles: [], 12 | }; 13 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamolegga/nestjs-pino/4bee74f4f9df4484f7c5f399e6ecc15f82293bc4/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-pino", 3 | "version": "4.4.0", 4 | "description": "Pino logger for NestJS", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "test": "jest --verbose -i --detectOpenHandles --forceExit", 9 | "lint": "tsc --noemit && eslint \"{src,__tests__}/**/*.ts\" --fix", 10 | "prebuild": "rimraf dist", 11 | "build": "tsc -p tsconfig.build.json", 12 | "example": "rimraf dist && tsc && LOG_LEVEL=debug node dist/example/main", 13 | "prepublishOnly": "npm run build && cp -r ./dist/* .", 14 | "postpublish": "git clean -fd", 15 | "postinstall": "node postinstall.js" 16 | }, 17 | "files": [ 18 | "*.{js,d.ts,map}", 19 | "!jest.config.js", 20 | "!.eslintrc.js" 21 | ], 22 | "engineStrict": true, 23 | "engines": { 24 | "node": ">= 14" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/iamolegga/nestjs-pino.git" 29 | }, 30 | "keywords": [ 31 | "pino", 32 | "nestjs", 33 | "nest.js", 34 | "nest", 35 | "logger" 36 | ], 37 | "author": "iamolegga (http://github.com/iamolegga)", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/iamolegga/nestjs-pino/issues" 41 | }, 42 | "homepage": "https://github.com/iamolegga/nestjs-pino#readme", 43 | "devDependencies": { 44 | "@eslint/compat": "^1.2.5", 45 | "@eslint/eslintrc": "^3.2.0", 46 | "@eslint/js": "^9.18.0", 47 | "@nestjs/common": "^11.0.3", 48 | "@nestjs/core": "^11.0.3", 49 | "@nestjs/platform-express": "^11.0.3", 50 | "@nestjs/platform-fastify": "^11.0.3", 51 | "@nestjs/testing": "^11.0.3", 52 | "@types/express": "^5.0.0", 53 | "@types/jest": "^29.5.14", 54 | "@types/memorystream": "^0.3.4", 55 | "@types/node": "^22.10.7", 56 | "@types/supertest": "^6.0.2", 57 | "@typescript-eslint/eslint-plugin": "^8.21.0", 58 | "@typescript-eslint/parser": "^8.21.0", 59 | "eslint": "^9.18.0", 60 | "eslint-config-prettier": "^10.0.1", 61 | "eslint-plugin-import": "^2.31.0", 62 | "eslint-plugin-prettier": "^5.2.3", 63 | "globals": "^16.0.0", 64 | "jest": "29.7.0", 65 | "memorystream": "^0.3.1", 66 | "pino": "^9.6.0", 67 | "pino-http": "^10.4.0", 68 | "prettier": "^3.4.2", 69 | "reflect-metadata": "^0.1.14", 70 | "rimraf": "^6.0.1", 71 | "rxjs": "^7.8.1", 72 | "supertest": "^7.0.0", 73 | "ts-jest": "^29.2.5", 74 | "ts-loader": "^9.5.2", 75 | "ts-node": "^10.9.2", 76 | "typescript": "^5.7.3" 77 | }, 78 | "peerDependencies": { 79 | "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", 80 | "pino": "^7.5.0 || ^8.0.0 || ^9.0.0", 81 | "pino-http": "^6.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", 82 | "rxjs": "^7.1.0" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /postinstall.js: -------------------------------------------------------------------------------- 1 | console.log(` 2 | 3 | 4 | 5 | 6 | \t\tThanks for using nestjs-pino! 7 | 8 | \t\tAs you may know there is a war against Ukraine: 9 | \t\tbombed cities (including schools, hospitals, etc), hundreds of killed children, millions of refugees. 10 | \t\tSo if you enjoy this library you can thank the author by donating to one of this funds: 11 | \t\t• https://war.ukraine.ua/donate/ – Armed Forces of Ukraine 12 | \t\t• https://savelife.in.ua/en/ – "The Come Back Alive" foundation 13 | 14 | \t\tThanks for your support! 🇺🇦 15 | 16 | `); 17 | 18 | new Promise((r) => setTimeout(r, 5000)); 19 | -------------------------------------------------------------------------------- /src/InjectPinoLogger.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Provider } from '@nestjs/common'; 2 | 3 | import { PinoLogger } from './PinoLogger'; 4 | 5 | const decoratedTokenPrefix = 'PinoLogger:'; 6 | 7 | const decoratedLoggers = new Set(); 8 | 9 | export function InjectPinoLogger(context = '') { 10 | decoratedLoggers.add(context); 11 | return Inject(getLoggerToken(context)); 12 | } 13 | 14 | function createDecoratedLoggerProvider(context: string): Provider { 15 | return { 16 | provide: getLoggerToken(context), 17 | useFactory: (logger: PinoLogger) => { 18 | logger.setContext(context); 19 | return logger; 20 | }, 21 | inject: [PinoLogger], 22 | }; 23 | } 24 | 25 | export function createProvidersForDecorated(): Array> { 26 | return [...decoratedLoggers.values()].map(createDecoratedLoggerProvider); 27 | } 28 | 29 | export function getLoggerToken(context: string): string { 30 | return `${decoratedTokenPrefix}${context}`; 31 | } 32 | -------------------------------------------------------------------------------- /src/Logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Injectable, LoggerService, Inject } from '@nestjs/common'; 3 | import { Level } from 'pino'; 4 | 5 | import { Params, PARAMS_PROVIDER_TOKEN } from './params'; 6 | import { PinoLogger } from './PinoLogger'; 7 | 8 | @Injectable() 9 | export class Logger implements LoggerService { 10 | private readonly contextName: string; 11 | 12 | constructor( 13 | protected readonly logger: PinoLogger, 14 | @Inject(PARAMS_PROVIDER_TOKEN) { renameContext }: Params, 15 | ) { 16 | this.contextName = renameContext || 'context'; 17 | } 18 | 19 | verbose(message: any, ...optionalParams: any[]) { 20 | this.call('trace', message, ...optionalParams); 21 | } 22 | 23 | debug(message: any, ...optionalParams: any[]) { 24 | this.call('debug', message, ...optionalParams); 25 | } 26 | 27 | log(message: any, ...optionalParams: any[]) { 28 | this.call('info', message, ...optionalParams); 29 | } 30 | 31 | warn(message: any, ...optionalParams: any[]) { 32 | this.call('warn', message, ...optionalParams); 33 | } 34 | 35 | error(message: any, ...optionalParams: any[]) { 36 | this.call('error', message, ...optionalParams); 37 | } 38 | 39 | fatal(message: any, ...optionalParams: any[]) { 40 | this.call('fatal', message, ...optionalParams); 41 | } 42 | 43 | private call(level: Level, message: any, ...optionalParams: any[]) { 44 | const objArg: Record = {}; 45 | 46 | // optionalParams contains extra params passed to logger 47 | // context name is the last item 48 | let params: any[] = []; 49 | if (optionalParams.length !== 0) { 50 | objArg[this.contextName] = optionalParams[optionalParams.length - 1]; 51 | params = optionalParams.slice(0, -1); 52 | } 53 | 54 | if (typeof message === 'object') { 55 | if (message instanceof Error) { 56 | objArg.err = message; 57 | } else { 58 | Object.assign(objArg, message); 59 | } 60 | this.logger[level](objArg, ...params); 61 | } else if (this.isWrongExceptionsHandlerContract(level, message, params)) { 62 | objArg.err = new Error(message); 63 | objArg.err.stack = params[0]; 64 | this.logger[level](objArg); 65 | } else { 66 | this.logger[level](objArg, message, ...params); 67 | } 68 | } 69 | 70 | /** 71 | * Unfortunately built-in (not only) `^.*Exception(s?)Handler$` classes call `.error` 72 | * method with not supported contract: 73 | * 74 | * - ExceptionsHandler 75 | * @see https://github.com/nestjs/nest/blob/35baf7a077bb972469097c5fea2f184b7babadfc/packages/core/exceptions/base-exception-filter.ts#L60-L63 76 | * 77 | * - ExceptionHandler 78 | * @see https://github.com/nestjs/nest/blob/99ee3fd99341bcddfa408d1604050a9571b19bc9/packages/core/errors/exception-handler.ts#L9 79 | * 80 | * - WsExceptionsHandler 81 | * @see https://github.com/nestjs/nest/blob/9d0551ff25c5085703bcebfa7ff3b6952869e794/packages/websockets/exceptions/base-ws-exception-filter.ts#L47-L50 82 | * 83 | * - RpcExceptionsHandler @see https://github.com/nestjs/nest/blob/9d0551ff25c5085703bcebfa7ff3b6952869e794/packages/microservices/exceptions/base-rpc-exception-filter.ts#L26-L30 84 | * 85 | * - all of them 86 | * @see https://github.com/search?l=TypeScript&q=org%3Anestjs+logger+error+stack&type=Code 87 | */ 88 | private isWrongExceptionsHandlerContract( 89 | level: Level, 90 | message: any, 91 | params: any[], 92 | ): params is [string] { 93 | return ( 94 | level === 'error' && 95 | typeof message === 'string' && 96 | params.length === 1 && 97 | typeof params[0] === 'string' && 98 | /\n\s*at /.test(params[0]) 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/LoggerErrorInterceptor.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { 3 | CallHandler, 4 | ExecutionContext, 5 | Injectable, 6 | NestInterceptor, 7 | } from '@nestjs/common'; 8 | import { catchError, Observable, throwError } from 'rxjs'; 9 | 10 | @Injectable() 11 | export class LoggerErrorInterceptor implements NestInterceptor { 12 | intercept( 13 | context: ExecutionContext, 14 | next: CallHandler, 15 | ): Observable | Promise> { 16 | return next.handle().pipe( 17 | catchError((error) => { 18 | return throwError(() => { 19 | const response = context.switchToHttp().getResponse(); 20 | 21 | const isFastifyResponse = response.raw !== undefined; 22 | 23 | if (isFastifyResponse) { 24 | response.raw.err = error; 25 | } else { 26 | response.err = error; 27 | } 28 | 29 | return error; 30 | }); 31 | }), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/LoggerModule.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { IncomingMessage, ServerResponse } from 'node:http'; 3 | 4 | import { 5 | Global, 6 | Module, 7 | DynamicModule, 8 | NestModule, 9 | MiddlewareConsumer, 10 | RequestMethod, 11 | Inject, 12 | } from '@nestjs/common'; 13 | import { Provider } from '@nestjs/common/interfaces'; 14 | import { pinoHttp } from 'pino-http'; 15 | 16 | import { createProvidersForDecorated } from './InjectPinoLogger'; 17 | import { Logger } from './Logger'; 18 | import { 19 | Params, 20 | LoggerModuleAsyncParams, 21 | PARAMS_PROVIDER_TOKEN, 22 | } from './params'; 23 | import { PinoLogger } from './PinoLogger'; 24 | import { Store, storage } from './storage'; 25 | 26 | /** 27 | * As NestJS@11 still supports express@4 `*`-style routing by itself let's keep 28 | * it for the backward compatibility. On the next major NestJS release `*` we 29 | * can replace it with `/{*splat}`, and drop the support for NestJS@9 and below. 30 | */ 31 | const DEFAULT_ROUTES = [{ path: '*', method: RequestMethod.ALL }]; 32 | 33 | @Global() 34 | @Module({ providers: [Logger], exports: [Logger] }) 35 | export class LoggerModule implements NestModule { 36 | static forRoot(params?: Params | undefined): DynamicModule { 37 | const paramsProvider: Provider = { 38 | provide: PARAMS_PROVIDER_TOKEN, 39 | useValue: params || {}, 40 | }; 41 | 42 | const decorated = createProvidersForDecorated(); 43 | 44 | return { 45 | module: LoggerModule, 46 | providers: [Logger, ...decorated, PinoLogger, paramsProvider], 47 | exports: [Logger, ...decorated, PinoLogger, paramsProvider], 48 | }; 49 | } 50 | 51 | static forRootAsync(params: LoggerModuleAsyncParams): DynamicModule { 52 | const paramsProvider: Provider> = { 53 | provide: PARAMS_PROVIDER_TOKEN, 54 | useFactory: params.useFactory, 55 | inject: params.inject, 56 | }; 57 | 58 | const decorated = createProvidersForDecorated(); 59 | 60 | const providers: any[] = [ 61 | Logger, 62 | ...decorated, 63 | PinoLogger, 64 | paramsProvider, 65 | ...(params.providers || []), 66 | ]; 67 | 68 | return { 69 | module: LoggerModule, 70 | imports: params.imports, 71 | providers, 72 | exports: [Logger, ...decorated, PinoLogger, paramsProvider], 73 | }; 74 | } 75 | 76 | constructor(@Inject(PARAMS_PROVIDER_TOKEN) private readonly params: Params) {} 77 | 78 | configure(consumer: MiddlewareConsumer) { 79 | const { 80 | exclude, 81 | forRoutes = DEFAULT_ROUTES, 82 | pinoHttp, 83 | useExisting, 84 | assignResponse, 85 | } = this.params; 86 | 87 | const middlewares = createLoggerMiddlewares( 88 | pinoHttp || {}, 89 | useExisting, 90 | assignResponse, 91 | ); 92 | 93 | if (exclude) { 94 | consumer 95 | .apply(...middlewares) 96 | .exclude(...exclude) 97 | .forRoutes(...forRoutes); 98 | } else { 99 | consumer.apply(...middlewares).forRoutes(...forRoutes); 100 | } 101 | } 102 | } 103 | 104 | function createLoggerMiddlewares( 105 | params: NonNullable, 106 | useExisting = false, 107 | assignResponse = false, 108 | ) { 109 | if (useExisting) { 110 | return [bindLoggerMiddlewareFactory(useExisting, assignResponse)]; 111 | } 112 | 113 | const middleware = pinoHttp( 114 | ...(Array.isArray(params) ? params : [params as any]), 115 | ); 116 | 117 | // @ts-expect-error: root is readonly field, but this is the place where 118 | // it's set actually 119 | PinoLogger.root = middleware.logger; 120 | 121 | // FIXME: params type here is pinoHttp.Options | pino.DestinationStream 122 | // pinoHttp has two overloads, each of them takes those types 123 | return [middleware, bindLoggerMiddlewareFactory(useExisting, assignResponse)]; 124 | } 125 | 126 | function bindLoggerMiddlewareFactory( 127 | useExisting: boolean, 128 | assignResponse: boolean, 129 | ) { 130 | return function bindLoggerMiddleware( 131 | req: IncomingMessage, 132 | res: ServerResponse, 133 | next: () => void, 134 | ) { 135 | let log = req.log; 136 | let resLog = assignResponse ? res.log : undefined; 137 | 138 | if (!useExisting && req.allLogs) { 139 | log = req.allLogs[req.allLogs.length - 1]!; 140 | } 141 | if (assignResponse && !useExisting && res.allLogs) { 142 | resLog = res.allLogs[res.allLogs.length - 1]!; 143 | } 144 | 145 | storage.run(new Store(log, resLog), next); 146 | }; 147 | } 148 | -------------------------------------------------------------------------------- /src/PinoLogger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Injectable, Inject, Scope } from '@nestjs/common'; 3 | import pino from 'pino'; 4 | 5 | import { Params, isPassedLogger, PARAMS_PROVIDER_TOKEN } from './params'; 6 | import { storage } from './storage'; 7 | 8 | type PinoMethods = Pick< 9 | pino.Logger, 10 | 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' 11 | >; 12 | 13 | /** 14 | * This is copy of pino.LogFn but with possibilty to make method override. 15 | * Current usage works: 16 | * 17 | * trace(msg: string, ...args: any[]): void; 18 | * trace(obj: object, msg?: string, ...args: any[]): void; 19 | * trace(...args: Parameters) { 20 | * this.call('trace', ...args); 21 | * } 22 | * 23 | * But if change local LoggerFn to pino.LogFn – this will say that overrides 24 | * are incompatible 25 | */ 26 | type LoggerFn = 27 | | ((msg: string, ...args: any[]) => void) 28 | | ((obj: object, msg?: string, ...args: any[]) => void); 29 | 30 | let outOfContext: pino.Logger | undefined; 31 | 32 | export function __resetOutOfContextForTests() { 33 | outOfContext = undefined; 34 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 35 | // @ts-ignore reset root for tests only 36 | PinoLogger.root = undefined; 37 | } 38 | 39 | @Injectable({ scope: Scope.TRANSIENT }) 40 | export class PinoLogger implements PinoMethods { 41 | /** 42 | * root is the most root logger that can be used to change params at runtime. 43 | * Accessible only when `useExisting` is not set to `true` in `Params`. 44 | * Readonly, but you can change it's properties. 45 | */ 46 | static readonly root: pino.Logger; 47 | 48 | protected context = ''; 49 | protected readonly contextName: string; 50 | protected readonly errorKey: string = 'err'; 51 | 52 | constructor( 53 | @Inject(PARAMS_PROVIDER_TOKEN) { pinoHttp, renameContext }: Params, 54 | ) { 55 | if ( 56 | typeof pinoHttp === 'object' && 57 | 'customAttributeKeys' in pinoHttp && 58 | typeof pinoHttp.customAttributeKeys !== 'undefined' 59 | ) { 60 | this.errorKey = pinoHttp.customAttributeKeys.err ?? 'err'; 61 | } 62 | 63 | if (!outOfContext) { 64 | if (Array.isArray(pinoHttp)) { 65 | outOfContext = pino(...pinoHttp); 66 | } else if (isPassedLogger(pinoHttp)) { 67 | outOfContext = pinoHttp.logger; 68 | } else if ( 69 | typeof pinoHttp === 'object' && 70 | 'stream' in pinoHttp && 71 | typeof pinoHttp.stream !== 'undefined' 72 | ) { 73 | outOfContext = pino(pinoHttp, pinoHttp.stream); 74 | } else { 75 | outOfContext = pino(pinoHttp); 76 | } 77 | } 78 | 79 | this.contextName = renameContext || 'context'; 80 | } 81 | 82 | get logger(): pino.Logger { 83 | // outOfContext is always set in runtime before starts using 84 | 85 | return storage.getStore()?.logger || outOfContext!; 86 | } 87 | 88 | trace(msg: string, ...args: any[]): void; 89 | trace(obj: unknown, msg?: string, ...args: any[]): void; 90 | trace(...args: Parameters) { 91 | this.call('trace', ...args); 92 | } 93 | 94 | debug(msg: string, ...args: any[]): void; 95 | debug(obj: unknown, msg?: string, ...args: any[]): void; 96 | debug(...args: Parameters) { 97 | this.call('debug', ...args); 98 | } 99 | 100 | info(msg: string, ...args: any[]): void; 101 | info(obj: unknown, msg?: string, ...args: any[]): void; 102 | info(...args: Parameters) { 103 | this.call('info', ...args); 104 | } 105 | 106 | warn(msg: string, ...args: any[]): void; 107 | warn(obj: unknown, msg?: string, ...args: any[]): void; 108 | warn(...args: Parameters) { 109 | this.call('warn', ...args); 110 | } 111 | 112 | error(msg: string, ...args: any[]): void; 113 | error(obj: unknown, msg?: string, ...args: any[]): void; 114 | error(...args: Parameters) { 115 | this.call('error', ...args); 116 | } 117 | 118 | fatal(msg: string, ...args: any[]): void; 119 | fatal(obj: unknown, msg?: string, ...args: any[]): void; 120 | fatal(...args: Parameters) { 121 | this.call('fatal', ...args); 122 | } 123 | 124 | setContext(value: string) { 125 | this.context = value; 126 | } 127 | 128 | assign(fields: pino.Bindings) { 129 | const store = storage.getStore(); 130 | if (!store) { 131 | throw new Error( 132 | `${PinoLogger.name}: unable to assign extra fields out of request scope`, 133 | ); 134 | } 135 | store.logger = store.logger.child(fields); 136 | store.responseLogger?.setBindings(fields); 137 | } 138 | 139 | protected call(method: pino.Level, ...args: Parameters) { 140 | if (this.context) { 141 | if (isFirstArgObject(args)) { 142 | const firstArg = args[0]; 143 | if (firstArg instanceof Error) { 144 | args = [ 145 | Object.assign( 146 | { [this.contextName]: this.context }, 147 | { [this.errorKey]: firstArg }, 148 | ), 149 | ...args.slice(1), 150 | ]; 151 | } else { 152 | args = [ 153 | Object.assign({ [this.contextName]: this.context }, firstArg), 154 | ...args.slice(1), 155 | ]; 156 | } 157 | } else { 158 | args = [{ [this.contextName]: this.context }, ...args]; 159 | } 160 | } 161 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 162 | // @ts-ignore args are union of tuple types 163 | this.logger[method](...args); 164 | } 165 | } 166 | 167 | function isFirstArgObject( 168 | args: Parameters, 169 | ): args is [obj: object, msg?: string, ...args: any[]] { 170 | return typeof args[0] === 'object'; 171 | } 172 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { LoggerModule } from './LoggerModule'; 2 | export { Logger } from './Logger'; 3 | export { PinoLogger } from './PinoLogger'; 4 | export { InjectPinoLogger, getLoggerToken } from './InjectPinoLogger'; 5 | export { LoggerErrorInterceptor } from './LoggerErrorInterceptor'; 6 | export { 7 | Params, 8 | LoggerModuleAsyncParams, 9 | PARAMS_PROVIDER_TOKEN, 10 | } from './params'; 11 | -------------------------------------------------------------------------------- /src/params.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { 3 | MiddlewareConfigProxy, 4 | ModuleMetadata, 5 | } from '@nestjs/common/interfaces'; 6 | import { Logger, DestinationStream } from 'pino'; 7 | import { Options } from 'pino-http'; 8 | 9 | export type PassedLogger = { logger: Logger }; 10 | 11 | export interface Params { 12 | /** 13 | * Optional parameters for `pino-http` module 14 | * @see https://github.com/pinojs/pino-http#pinohttpopts-stream 15 | */ 16 | pinoHttp?: Options | DestinationStream | [Options, DestinationStream]; 17 | 18 | /** 19 | * Optional parameter for routing. It should implement interface of 20 | * parameters of NestJS built-in `MiddlewareConfigProxy['forRoutes']`. 21 | * @see https://docs.nestjs.com/middleware#applying-middleware 22 | * It can be used for both disabling automatic req/res logs and 23 | * removing request context from following logs. It works for all requests by 24 | * default. If you only need to turn off the automatic request/response 25 | * logging for some specific (or all) routes but keep request context for app 26 | * logs use `pinoHttp.autoLogging` field. 27 | */ 28 | exclude?: Parameters; 29 | 30 | /** 31 | * Optional parameter for routing. It should implement interface of 32 | * parameters of NestJS built-in `MiddlewareConfigProxy['forRoutes']`. 33 | * @see https://docs.nestjs.com/middleware#applying-middleware 34 | * It can be used for both disabling automatic req/res logs and 35 | * removing request context from following logs. It works for all requests by 36 | * default. If you only need to turn off the automatic request/response 37 | * logging for some specific (or all) routes but keep request context for app 38 | * logs use `pinoHttp.autoLogging` field. 39 | */ 40 | forRoutes?: Parameters; 41 | 42 | /** 43 | * Optional parameter to skip pino configuration in case you are using 44 | * FastifyAdapter, and already configure logger in adapter's config. The Pros 45 | * and cons of this approach are described in the FAQ section of the 46 | * documentation: 47 | * @see https://github.com/iamolegga/nestjs-pino#faq. 48 | */ 49 | useExisting?: true; 50 | 51 | /** 52 | * Optional parameter to change property name `context` in resulted logs, 53 | * so logs will be like: 54 | * {"level":30, ... "RENAME_CONTEXT_VALUE_HERE":"AppController" } 55 | */ 56 | renameContext?: string; 57 | 58 | /** 59 | * Optional parameter to also assign the response logger during calls to 60 | * `PinoLogger.assign`. By default, `assign` does not impact response logs 61 | * (e.g.`Request completed`). 62 | */ 63 | assignResponse?: boolean; 64 | } 65 | 66 | // for support of nestjs@8 we don't use 67 | // extends Pick 68 | // as it's `useFactory` return type in v8 is `T` instead of `T | Promise` as 69 | // in feature versions, so it's not compatible 70 | export interface LoggerModuleAsyncParams 71 | extends Pick { 72 | useFactory: (...args: any[]) => Params | Promise; 73 | inject?: any[]; 74 | } 75 | 76 | export function isPassedLogger( 77 | pinoHttpProp: any, 78 | ): pinoHttpProp is PassedLogger { 79 | return !!pinoHttpProp && 'logger' in pinoHttpProp; 80 | } 81 | 82 | export const PARAMS_PROVIDER_TOKEN = 'pino-params'; 83 | -------------------------------------------------------------------------------- /src/storage.ts: -------------------------------------------------------------------------------- 1 | import { AsyncLocalStorage } from 'async_hooks'; 2 | 3 | import { Logger } from 'pino'; 4 | 5 | export class Store { 6 | constructor( 7 | public logger: Logger, 8 | public responseLogger?: Logger, 9 | ) {} 10 | } 11 | 12 | export const storage = new AsyncLocalStorage(); 13 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "dist", "__tests__", "example"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2019"], 4 | "module": "commonjs", 5 | "target": "ES2019", 6 | "declaration": true, 7 | "removeComments": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | "inlineSources": true, 12 | "outDir": "./dist", 13 | "incremental": true, 14 | "allowSyntheticDefaultImports": true, 15 | "strict": true, 16 | "skipLibCheck": true, 17 | "noUncheckedIndexedAccess": true 18 | } 19 | } 20 | --------------------------------------------------------------------------------