From f90b802419339e99d74faf5439c3bdfdedcd7e3e Mon Sep 17 00:00:00 2001 From: radmehr Date: Sat, 5 Sep 2026 16:50:15 +0330 Subject: [PATCH] feat: init logging system --- package-lock.json | 252 ++++++++++++++++++ package.json | 4 + src/app.module.ts | 5 + src/common/filters/global-exception.filter.ts | 40 ++- src/common/modules/logging/logging.module.ts | 81 ++++++ src/config/app.config.ts | 1 + src/config/environment.validation.ts | 1 + src/main.ts | 5 +- 8 files changed, 384 insertions(+), 5 deletions(-) create mode 100644 src/common/modules/logging/logging.module.ts diff --git a/package-lock.json b/package-lock.json index 2288c21..be9534f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,9 +27,12 @@ "ioredis": "^5.10.1", "joi": "^18.0.2", "nestjs-i18n": "^10.6.0", + "nestjs-pino": "^5.1.0", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "pg": "^8.18.0", + "pino": "^10.3.1", + "pino-http": "^11.0.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "typeorm": "^0.3.28" @@ -52,6 +55,7 @@ "eslint-plugin-prettier": "^5.2.2", "globals": "^16.0.0", "jest": "^29.7.0", + "pino-pretty": "^13.1.3", "prettier": "^3.4.2", "source-map-support": "^0.5.21", "supertest": "^7.0.0", @@ -3092,6 +3096,11 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4292,6 +4301,14 @@ "dev": true, "license": "MIT" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -5010,6 +5027,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5255,6 +5278,15 @@ "node": ">= 8" } }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/dayjs": { "version": "1.11.21", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", @@ -5517,6 +5549,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.24.5", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", @@ -5998,6 +6039,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fast-copy": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.1.1.tgz", + "integrity": "sha512-A4QTJmuiztpGtr6AMeJts9R4hbj2ZBUwtOaKrG6rw2y7t6+IaJKjz5M3XDs8BUznxDH43FVc6A0y/gWlMl4UtA==", + "dev": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6659,6 +6706,12 @@ "url": "https://github.com/sponsors/EvanHahn" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -7787,6 +7840,15 @@ "node": ">= 20" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8743,6 +8805,21 @@ "node": ">=8.10.0" } }, + "node_modules/nestjs-pino": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/nestjs-pino/-/nestjs-pino-5.1.0.tgz", + "integrity": "sha512-13XH4Z4gabhBh9BTIcGACvetoXlsT23SCnuaB8OVs1fKnUWQQbp5T3BnSPAdMM1NcL5a95s0M+e1Jds312ouNA==", + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.8 || ^12.0.0", + "@nestjs/core": "^11.0.8 || ^12.0.0", + "pino": "^10.0.0", + "pino-http": "^11.0.0", + "rxjs": "^7.1.0" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -8840,6 +8917,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -9241,6 +9326,87 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-http": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-11.0.0.tgz", + "integrity": "sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==", + "dependencies": { + "get-caller-file": "^2.0.5", + "pino": "^10.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "dev": true, + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==" + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -9445,6 +9611,21 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -9472,6 +9653,16 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -9515,6 +9706,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, "node_modules/range-parser": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", @@ -9578,6 +9774,14 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", @@ -9761,6 +9965,14 @@ "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", "license": "ISC" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -9786,6 +9998,22 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -10008,6 +10236,14 @@ "node": ">=8" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -10590,6 +10826,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==" + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", diff --git a/package.json b/package.json index 2ff6018..d223ff0 100644 --- a/package.json +++ b/package.json @@ -48,9 +48,12 @@ "ioredis": "^5.10.1", "joi": "^18.0.2", "nestjs-i18n": "^10.6.0", + "nestjs-pino": "^5.1.0", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "pg": "^8.18.0", + "pino": "^10.3.1", + "pino-http": "^11.0.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "typeorm": "^0.3.28" @@ -73,6 +76,7 @@ "eslint-plugin-prettier": "^5.2.2", "globals": "^16.0.0", "jest": "^29.7.0", + "pino-pretty": "^13.1.3", "prettier": "^3.4.2", "source-map-support": "^0.5.21", "supertest": "^7.0.0", diff --git a/src/app.module.ts b/src/app.module.ts index fc03307..57c316e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -23,6 +23,7 @@ import { ScoringModule } from './modules/scoring/scoring.module'; import { AdminModule } from './modules/admin/admin.module'; import { AppThrottlerGuard } from './common/guards/app-throttler.guard'; import { ThrottlerModule } from '@nestjs/throttler'; +import { LoggingModule } from './common/modules/logging/logging.module'; const ENV = process.env.NODE_ENV; @@ -79,6 +80,9 @@ const ENV = process.env.NODE_ENV; // ]), ThrottlerModule.forRoot([]), + // Logging + LoggingModule, + /** * Local Modules */ @@ -92,6 +96,7 @@ const ENV = process.env.NODE_ENV; SubmissionsModule, ScoringModule, AdminModule, + LoggingModule, ], providers: [ { diff --git a/src/common/filters/global-exception.filter.ts b/src/common/filters/global-exception.filter.ts index b7a109a..95a4fd0 100644 --- a/src/common/filters/global-exception.filter.ts +++ b/src/common/filters/global-exception.filter.ts @@ -8,6 +8,7 @@ import { } from '@nestjs/common'; import type { Request, Response } from 'express'; import { I18nService } from 'nestjs-i18n'; +import { PinoLogger } from 'nestjs-pino'; @Catch() export class GlobalExceptionFilter implements ExceptionFilter { @@ -16,6 +17,11 @@ export class GlobalExceptionFilter implements ExceptionFilter { * Inject I18n Service */ private readonly i18nService: I18nService, + + /** + * Inject Pino Logger + */ + private readonly logger: PinoLogger, ) {} catch(exception: any, host: ArgumentsHost) { @@ -40,16 +46,42 @@ export class GlobalExceptionFilter implements ExceptionFilter { args: (exceptionResponse as any).params, }); + this.logException(request, status, exception); + response.status(status).json({ success: false, statusCode: status, message, path: request.url, timestamp: new Date().toISOString(), - // ...(process.env.NODE_ENV !== 'production' && { - raw: exceptionResponse, - stack: exception instanceof Error ? exception.stack : undefined, - // }), + ...(process.env.NODE_ENV !== 'production' && { + raw: exceptionResponse, + stack: exception instanceof Error ? exception.stack : undefined, + }), }); } + + logException(request: Request, status: number, exception?: any) { + if (status >= 500) { + this.logger.error( + { + err: exception, + stack: exception instanceof Error ? exception.stack : undefined, + method: request.method, + url: request.url, + statusCode: status, + }, + 'Unhandled server exception', + ); + } else { + this.logger.warn( + { + method: request.method, + url: request.url, + statusCode: status, + }, + 'HTTP exception', + ); + } + } } diff --git a/src/common/modules/logging/logging.module.ts b/src/common/modules/logging/logging.module.ts new file mode 100644 index 0000000..7a0301f --- /dev/null +++ b/src/common/modules/logging/logging.module.ts @@ -0,0 +1,81 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { LoggerModule } from 'nestjs-pino'; + +@Module({ + imports: [ + LoggerModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + pinoHttp: { + level: configService.get('app.logLevel') ?? 'info', + genReqId: (req) => { + const requestId = req.headers['x-request-id']; + + if (typeof requestId === 'string' && requestId.length > 0) { + return requestId; + } + + return crypto.randomUUID(); + }, + redact: { + paths: [ + 'req.headers.authorization', + 'req.headers.cookie', + 'req.headers["set-cookie"]', + 'password', + 'accessToken', + 'refreshToken', + 'otp', + ], + censor: '[REDACTED]', + }, + + // serializers: { + // req: (req) => ({ + // method: req.method, + // url: req.url, + // remoteAddress: req.remoteAddress, + // }), + + // res: (res) => ({ + // statusCode: res.statusCode, + // }), + // }, + + customProps: (req) => ({ + requestId: req.id, + }), + + customLogLevel: (req, res, error) => { + if (error || res.statusCode >= 500) { + return 'error'; + } + + if (res.statusCode >= 400) { + return 'warn'; + } + + return 'info'; + }, + + transport: + configService.get('app.environment') !== 'production' + ? { + target: 'pino-pretty', + options: { + colorize: true, + singleLine: true, + translateTime: 'SYS:standard', + ignore: 'pid,hostname', + }, + } + : undefined, + }, + }), + }), + ], + exports: [LoggerModule], +}) +export class LoggingModule {} diff --git a/src/config/app.config.ts b/src/config/app.config.ts index 3e0d79a..3b41710 100644 --- a/src/config/app.config.ts +++ b/src/config/app.config.ts @@ -9,4 +9,5 @@ export default registerAs('app', () => ({ swaggerServers: process.env.SWAGGER_SERVERS?.split(',').map((origin) => origin.trim(), ), + logLevel: process.env.LOG_LEVEL, })); diff --git a/src/config/environment.validation.ts b/src/config/environment.validation.ts index b11ebf3..096bf6a 100644 --- a/src/config/environment.validation.ts +++ b/src/config/environment.validation.ts @@ -22,4 +22,5 @@ export default Joi.object({ CORS_ORIGINS: Joi.string().required(), CORS_SUBDOMAIN: Joi.string().required(), SWAGGER_SERVERS: Joi.string().required(), + LOG_LEVEL: Joi.string().default('info'), }); diff --git a/src/main.ts b/src/main.ts index 1198444..4939023 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,9 +5,12 @@ import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { ResponseInterceptor } from './common/interceptors/response.interceptor'; import helmet from 'helmet'; import { ConfigService } from '@nestjs/config'; +import { Logger } from 'nestjs-pino'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { bufferLogs: true }); + + app.useLogger(app.get(Logger)); app.setGlobalPrefix('v1');