From 9bdee873ccac66c939790b86646fd5c48a4dcb24 Mon Sep 17 00:00:00 2001
From: Faraz Razi <72218210+FarazRazi@users.noreply.github.com>
Date: Mon, 21 Apr 2025 13:03:54 +0000
Subject: [PATCH 1/2] feat(logger): add request logging interceptor with
detailed and concise logging
---
env-example-document | 4 +
env-example-relational | 4 +
package-lock.json | 229 +++++++++++++++++++++++++
package.json | 3 +
src/app.module.ts | 196 ++++++++++-----------
src/config/config.type.ts | 40 +++--
src/config/logger-config.type.ts | 4 +
src/config/logger.config.ts | 32 ++++
src/database/typeorm-config.service.ts | 114 ++++++------
src/logger/logger.interceptor.ts | 120 +++++++++++++
src/logger/logger.module.ts | 14 ++
src/logger/logger.service.ts | 51 ++++++
src/main.ts | 126 +++++++-------
13 files changed, 705 insertions(+), 232 deletions(-)
create mode 100644 src/config/logger-config.type.ts
create mode 100644 src/config/logger.config.ts
create mode 100644 src/logger/logger.interceptor.ts
create mode 100644 src/logger/logger.module.ts
create mode 100644 src/logger/logger.service.ts
diff --git a/env-example-document b/env-example-document
index c5ab965f6..0fc617391 100644
--- a/env-example-document
+++ b/env-example-document
@@ -50,3 +50,7 @@ GOOGLE_CLIENT_SECRET=
APPLE_APP_AUDIENCE=[]
WORKER_HOST=redis://redis:6379/1
+
+# Logger Configuration
+LOG_LEVEL=info
+LOG_PRETTY=true
\ No newline at end of file
diff --git a/env-example-relational b/env-example-relational
index 1501e81ec..f8e688e5e 100644
--- a/env-example-relational
+++ b/env-example-relational
@@ -58,3 +58,7 @@ GOOGLE_CLIENT_SECRET=
APPLE_APP_AUDIENCE=[]
WORKER_HOST=redis://redis:6379/1
+
+# Logger Configuration
+LOG_LEVEL=info
+LOG_PRETTY=true
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index c40384651..f3bf49c04 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,6 +21,7 @@
"@nestjs/swagger": "11.1.4",
"@nestjs/typeorm": "11.0.0",
"@types/multer-s3": "3.0.3",
+ "@types/pino": "^7.0.5",
"@types/prompts": "2.4.9",
"apple-signin-auth": "1.7.9",
"bcryptjs": "3.0.2",
@@ -80,6 +81,8 @@
"hygen": "6.2.11",
"is-ci": "4.1.0",
"jest": "29.7.0",
+ "pino": "^9.6.0",
+ "pino-pretty": "^13.0.0",
"prettier": "3.5.3",
"prompts": "2.4.2",
"release-it": "18.1.2",
@@ -6569,6 +6572,16 @@
"@types/passport": "*"
}
},
+ "node_modules/@types/pino": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/@types/pino/-/pino-7.0.5.tgz",
+ "integrity": "sha512-wKoab31pknvILkxAF8ss+v9iNyhw5Iu/0jLtRkUD74cNfOOLJNnqfFKAv0r7wVaTQxRZtWrMpGfShwwBjOcgcg==",
+ "deprecated": "This is a stub types definition. pino provides its own type definitions, so you do not need this installed.",
+ "license": "MIT",
+ "dependencies": {
+ "pino": "*"
+ }
+ },
"node_modules/@types/prompts": {
"version": "2.4.9",
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz",
@@ -8188,6 +8201,15 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
+ "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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/atomically": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/atomically/-/atomically-2.0.3.tgz",
@@ -9210,6 +9232,13 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
+ "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,
+ "license": "MIT"
+ },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -10071,6 +10100,16 @@
"node": ">= 14"
}
},
+ "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,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
@@ -10440,6 +10479,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.17.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
@@ -11267,6 +11316,13 @@
],
"license": "MIT"
},
+ "node_modules/fast-copy": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
+ "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -11312,6 +11368,15 @@
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
},
+ "node_modules/fast-redact": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
+ "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
@@ -12399,6 +12464,13 @@
"upper-case": "^1.1.3"
}
},
+ "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,
+ "license": "MIT"
+ },
"node_modules/hexoid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
@@ -15121,6 +15193,16 @@
"jiti": "lib/jiti-cli.mjs"
}
},
+ "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,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -16468,6 +16550,15 @@
"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==",
+ "license": "MIT",
+ "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",
@@ -17129,6 +17220,68 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pino": {
+ "version": "9.6.0",
+ "resolved": "https://registry.npmjs.org/pino/-/pino-9.6.0.tgz",
+ "integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==",
+ "license": "MIT",
+ "dependencies": {
+ "atomic-sleep": "^1.0.0",
+ "fast-redact": "^3.1.1",
+ "on-exit-leak-free": "^2.1.0",
+ "pino-abstract-transport": "^2.0.0",
+ "pino-std-serializers": "^7.0.0",
+ "process-warning": "^4.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": "^3.0.0"
+ },
+ "bin": {
+ "pino": "bin.js"
+ }
+ },
+ "node_modules/pino-abstract-transport": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
+ "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.0.0"
+ }
+ },
+ "node_modules/pino-pretty": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz",
+ "integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "colorette": "^2.0.7",
+ "dateformat": "^4.6.3",
+ "fast-copy": "^3.0.2",
+ "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": "^2.0.0",
+ "pump": "^3.0.0",
+ "secure-json-parse": "^2.4.0",
+ "sonic-boom": "^4.0.1",
+ "strip-json-comments": "^3.1.1"
+ },
+ "bin": {
+ "pino-pretty": "bin.js"
+ }
+ },
+ "node_modules/pino-std-serializers": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
+ "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
+ "license": "MIT"
+ },
"node_modules/pirates": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz",
@@ -17325,6 +17478,22 @@
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
+ "node_modules/process-warning": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz",
+ "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -17406,6 +17575,17 @@
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
},
+ "node_modules/pump": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
+ "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
+ "dev": true,
+ "license": "MIT",
+ "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",
@@ -17468,6 +17648,12 @@
"dev": true,
"license": "MIT"
},
+ "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==",
+ "license": "MIT"
+ },
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
@@ -17668,6 +17854,15 @@
"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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12.13.0"
+ }
+ },
"node_modules/rechoir": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
@@ -18524,6 +18719,15 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
+ "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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -18547,6 +18751,13 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/secure-json-parse": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
+ "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/seek-bzip": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-2.0.0.tgz",
@@ -18891,6 +19102,15 @@
"node": ">= 14"
}
},
+ "node_modules/sonic-boom": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
+ "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
+ "license": "MIT",
+ "dependencies": {
+ "atomic-sleep": "^1.0.0"
+ }
+ },
"node_modules/sort-keys": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
@@ -19646,6 +19866,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/thread-stream": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
+ "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
+ "license": "MIT",
+ "dependencies": {
+ "real-require": "^0.2.0"
+ }
+ },
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
diff --git a/package.json b/package.json
index 929466263..e3f5e8bab 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,7 @@
"@nestjs/swagger": "11.1.4",
"@nestjs/typeorm": "11.0.0",
"@types/multer-s3": "3.0.3",
+ "@types/pino": "^7.0.5",
"@types/prompts": "2.4.9",
"apple-signin-auth": "1.7.9",
"bcryptjs": "3.0.2",
@@ -121,6 +122,8 @@
"hygen": "6.2.11",
"is-ci": "4.1.0",
"jest": "29.7.0",
+ "pino": "^9.6.0",
+ "pino-pretty": "^13.0.0",
"prettier": "3.5.3",
"prompts": "2.4.2",
"release-it": "18.1.2",
diff --git a/src/app.module.ts b/src/app.module.ts
index 1d13afb04..2985998ce 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -1,97 +1,99 @@
-import { Module } from '@nestjs/common';
-import { UsersModule } from './users/users.module';
-import { FilesModule } from './files/files.module';
-import { AuthModule } from './auth/auth.module';
-import databaseConfig from './database/config/database.config';
-import authConfig from './auth/config/auth.config';
-import appConfig from './config/app.config';
-import mailConfig from './mail/config/mail.config';
-import fileConfig from './files/config/file.config';
-import facebookConfig from './auth-facebook/config/facebook.config';
-import googleConfig from './auth-google/config/google.config';
-import appleConfig from './auth-apple/config/apple.config';
-import path from 'path';
-import { ConfigModule, ConfigService } from '@nestjs/config';
-import { TypeOrmModule } from '@nestjs/typeorm';
-import { AuthAppleModule } from './auth-apple/auth-apple.module';
-import { AuthFacebookModule } from './auth-facebook/auth-facebook.module';
-import { AuthGoogleModule } from './auth-google/auth-google.module';
-import { HeaderResolver, I18nModule } from 'nestjs-i18n';
-import { TypeOrmConfigService } from './database/typeorm-config.service';
-import { MailModule } from './mail/mail.module';
-import { HomeModule } from './home/home.module';
-import { DataSource, DataSourceOptions } from 'typeorm';
-import { AllConfigType } from './config/config.type';
-import { SessionModule } from './session/session.module';
-import { MailerModule } from './mailer/mailer.module';
-import { MongooseModule } from '@nestjs/mongoose';
-import { MongooseConfigService } from './database/mongoose-config.service';
-import { DatabaseConfig } from './database/config/database-config.type';
-
-//
-const infrastructureDatabaseModule = (databaseConfig() as DatabaseConfig)
- .isDocumentDatabase
- ? MongooseModule.forRootAsync({
- useClass: MongooseConfigService,
- })
- : TypeOrmModule.forRootAsync({
- useClass: TypeOrmConfigService,
- dataSourceFactory: async (options: DataSourceOptions) => {
- return new DataSource(options).initialize();
- },
- });
-//
-
-@Module({
- imports: [
- ConfigModule.forRoot({
- isGlobal: true,
- load: [
- databaseConfig,
- authConfig,
- appConfig,
- mailConfig,
- fileConfig,
- facebookConfig,
- googleConfig,
- appleConfig,
- ],
- envFilePath: ['.env'],
- }),
- infrastructureDatabaseModule,
- I18nModule.forRootAsync({
- useFactory: (configService: ConfigService) => ({
- fallbackLanguage: configService.getOrThrow('app.fallbackLanguage', {
- infer: true,
- }),
- loaderOptions: { path: path.join(__dirname, '/i18n/'), watch: true },
- }),
- resolvers: [
- {
- use: HeaderResolver,
- useFactory: (configService: ConfigService) => {
- return [
- configService.get('app.headerLanguage', {
- infer: true,
- }),
- ];
- },
- inject: [ConfigService],
- },
- ],
- imports: [ConfigModule],
- inject: [ConfigService],
- }),
- UsersModule,
- FilesModule,
- AuthModule,
- AuthFacebookModule,
- AuthGoogleModule,
- AuthAppleModule,
- SessionModule,
- MailModule,
- MailerModule,
- HomeModule,
- ],
-})
-export class AppModule {}
+import { Module } from '@nestjs/common';
+import { UsersModule } from './users/users.module';
+import { FilesModule } from './files/files.module';
+import { AuthModule } from './auth/auth.module';
+import databaseConfig from './database/config/database.config';
+import authConfig from './auth/config/auth.config';
+import appConfig from './config/app.config';
+import mailConfig from './mail/config/mail.config';
+import fileConfig from './files/config/file.config';
+import facebookConfig from './auth-facebook/config/facebook.config';
+import googleConfig from './auth-google/config/google.config';
+import appleConfig from './auth-apple/config/apple.config';
+import path from 'path';
+import { ConfigModule, ConfigService } from '@nestjs/config';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { AuthAppleModule } from './auth-apple/auth-apple.module';
+import { AuthFacebookModule } from './auth-facebook/auth-facebook.module';
+import { AuthGoogleModule } from './auth-google/auth-google.module';
+import { HeaderResolver, I18nModule } from 'nestjs-i18n';
+import { TypeOrmConfigService } from './database/typeorm-config.service';
+import { MailModule } from './mail/mail.module';
+import { HomeModule } from './home/home.module';
+import { DataSource, DataSourceOptions } from 'typeorm';
+import { AllConfigType } from './config/config.type';
+import { SessionModule } from './session/session.module';
+import { MailerModule } from './mailer/mailer.module';
+import { MongooseModule } from '@nestjs/mongoose';
+import { MongooseConfigService } from './database/mongoose-config.service';
+import { DatabaseConfig } from './database/config/database-config.type';
+import { LoggerModule } from './logger/logger.module';
+
+//
+const infrastructureDatabaseModule = (databaseConfig() as DatabaseConfig)
+ .isDocumentDatabase
+ ? MongooseModule.forRootAsync({
+ useClass: MongooseConfigService,
+ })
+ : TypeOrmModule.forRootAsync({
+ useClass: TypeOrmConfigService,
+ dataSourceFactory: async (options: DataSourceOptions) => {
+ return new DataSource(options).initialize();
+ },
+ });
+//
+
+@Module({
+ imports: [
+ ConfigModule.forRoot({
+ isGlobal: true,
+ load: [
+ databaseConfig,
+ authConfig,
+ appConfig,
+ mailConfig,
+ fileConfig,
+ facebookConfig,
+ googleConfig,
+ appleConfig,
+ ],
+ envFilePath: ['.env'],
+ }),
+ infrastructureDatabaseModule,
+ I18nModule.forRootAsync({
+ useFactory: (configService: ConfigService) => ({
+ fallbackLanguage: configService.getOrThrow('app.fallbackLanguage', {
+ infer: true,
+ }),
+ loaderOptions: { path: path.join(__dirname, '/i18n/'), watch: true },
+ }),
+ resolvers: [
+ {
+ use: HeaderResolver,
+ useFactory: (configService: ConfigService) => {
+ return [
+ configService.get('app.headerLanguage', {
+ infer: true,
+ }),
+ ];
+ },
+ inject: [ConfigService],
+ },
+ ],
+ imports: [ConfigModule],
+ inject: [ConfigService],
+ }),
+ UsersModule,
+ FilesModule,
+ AuthModule,
+ AuthFacebookModule,
+ AuthGoogleModule,
+ AuthAppleModule,
+ SessionModule,
+ MailModule,
+ MailerModule,
+ HomeModule,
+ LoggerModule,
+ ],
+})
+export class AppModule {}
diff --git a/src/config/config.type.ts b/src/config/config.type.ts
index 778e63746..409a5c57f 100644
--- a/src/config/config.type.ts
+++ b/src/config/config.type.ts
@@ -1,19 +1,21 @@
-import { AppConfig } from './app-config.type';
-import { AppleConfig } from '../auth-apple/config/apple-config.type';
-import { AuthConfig } from '../auth/config/auth-config.type';
-import { DatabaseConfig } from '../database/config/database-config.type';
-import { FacebookConfig } from '../auth-facebook/config/facebook-config.type';
-import { FileConfig } from '../files/config/file-config.type';
-import { GoogleConfig } from '../auth-google/config/google-config.type';
-import { MailConfig } from '../mail/config/mail-config.type';
-
-export type AllConfigType = {
- app: AppConfig;
- apple: AppleConfig;
- auth: AuthConfig;
- database: DatabaseConfig;
- facebook: FacebookConfig;
- file: FileConfig;
- google: GoogleConfig;
- mail: MailConfig;
-};
+import { AppleConfig } from '../auth-apple/config/apple-config.type';
+import { FacebookConfig } from '../auth-facebook/config/facebook-config.type';
+import { GoogleConfig } from '../auth-google/config/google-config.type';
+import { AuthConfig } from '../auth/config/auth-config.type';
+import { DatabaseConfig } from '../database/config/database-config.type';
+import { FileConfig } from '../files/config/file-config.type';
+import { MailConfig } from '../mail/config/mail-config.type';
+import { AppConfig } from './app-config.type';
+import { LoggerConfig } from './logger-config.type';
+
+export type AllConfigType = {
+ app: AppConfig;
+ apple: AppleConfig;
+ auth: AuthConfig;
+ database: DatabaseConfig;
+ facebook: FacebookConfig;
+ file: FileConfig;
+ google: GoogleConfig;
+ mail: MailConfig;
+ logger: LoggerConfig;
+};
diff --git a/src/config/logger-config.type.ts b/src/config/logger-config.type.ts
new file mode 100644
index 000000000..e9103d9dc
--- /dev/null
+++ b/src/config/logger-config.type.ts
@@ -0,0 +1,4 @@
+export interface LoggerConfig {
+ level: string;
+ pretty: boolean;
+}
diff --git a/src/config/logger.config.ts b/src/config/logger.config.ts
new file mode 100644
index 000000000..75c71a3aa
--- /dev/null
+++ b/src/config/logger.config.ts
@@ -0,0 +1,32 @@
+import { IsEnum, IsOptional, IsString } from 'class-validator';
+
+import { registerAs } from '@nestjs/config';
+
+import validateConfig from '../utils/validate-config';
+
+enum LogLevel {
+ ERROR = 'error',
+ WARN = 'warn',
+ INFO = 'info',
+ DEBUG = 'debug',
+ TRACE = 'trace',
+}
+
+class EnvironmentVariablesValidator {
+ @IsEnum(LogLevel)
+ @IsOptional()
+ LOG_LEVEL: LogLevel;
+
+ @IsString()
+ @IsOptional()
+ LOG_PRETTY: string;
+}
+
+export default registerAs('logger', () => {
+ validateConfig(process.env, EnvironmentVariablesValidator);
+
+ return {
+ level: process.env.LOG_LEVEL || 'info',
+ pretty: process.env.LOG_PRETTY === 'true',
+ };
+});
diff --git a/src/database/typeorm-config.service.ts b/src/database/typeorm-config.service.ts
index 45c79bc12..333299fa2 100644
--- a/src/database/typeorm-config.service.ts
+++ b/src/database/typeorm-config.service.ts
@@ -1,57 +1,57 @@
-import { Injectable } from '@nestjs/common';
-import { ConfigService } from '@nestjs/config';
-import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
-import { AllConfigType } from '../config/config.type';
-
-@Injectable()
-export class TypeOrmConfigService implements TypeOrmOptionsFactory {
- constructor(private configService: ConfigService) {}
-
- createTypeOrmOptions(): TypeOrmModuleOptions {
- return {
- type: this.configService.get('database.type', { infer: true }),
- url: this.configService.get('database.url', { infer: true }),
- host: this.configService.get('database.host', { infer: true }),
- port: this.configService.get('database.port', { infer: true }),
- username: this.configService.get('database.username', { infer: true }),
- password: this.configService.get('database.password', { infer: true }),
- database: this.configService.get('database.name', { infer: true }),
- synchronize: this.configService.get('database.synchronize', {
- infer: true,
- }),
- dropSchema: false,
- keepConnectionAlive: true,
- logging:
- this.configService.get('app.nodeEnv', { infer: true }) !== 'production',
- entities: [__dirname + '/../**/*.entity{.ts,.js}'],
- migrations: [__dirname + '/migrations/**/*{.ts,.js}'],
- cli: {
- entitiesDir: 'src',
-
- subscribersDir: 'subscriber',
- },
- extra: {
- // based on https://node-postgres.com/apis/pool
- // max connection pool size
- max: this.configService.get('database.maxConnections', { infer: true }),
- ssl: this.configService.get('database.sslEnabled', { infer: true })
- ? {
- rejectUnauthorized: this.configService.get(
- 'database.rejectUnauthorized',
- { infer: true },
- ),
- ca:
- this.configService.get('database.ca', { infer: true }) ??
- undefined,
- key:
- this.configService.get('database.key', { infer: true }) ??
- undefined,
- cert:
- this.configService.get('database.cert', { infer: true }) ??
- undefined,
- }
- : undefined,
- },
- } as TypeOrmModuleOptions;
- }
-}
+import { Injectable } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
+import { AllConfigType } from '../config/config.type';
+
+@Injectable()
+export class TypeOrmConfigService implements TypeOrmOptionsFactory {
+ constructor(private configService: ConfigService) {}
+
+ createTypeOrmOptions(): TypeOrmModuleOptions {
+ return {
+ type: this.configService.get('database.type', { infer: true }),
+ url: this.configService.get('database.url', { infer: true }),
+ host: this.configService.get('database.host', { infer: true }),
+ port: this.configService.get('database.port', { infer: true }),
+ username: this.configService.get('database.username', { infer: true }),
+ password: this.configService.get('database.password', { infer: true }),
+ database: this.configService.get('database.name', { infer: true }),
+ synchronize: this.configService.get('database.synchronize', {
+ infer: true,
+ }),
+ dropSchema: false,
+ keepConnectionAlive: true,
+ logging:
+ this.configService.get('logger.level', { infer: true }) === 'debug',
+ entities: [__dirname + '/../**/*.entity{.ts,.js}'],
+ migrations: [__dirname + '/migrations/**/*{.ts,.js}'],
+ cli: {
+ entitiesDir: 'src',
+
+ subscribersDir: 'subscriber',
+ },
+ extra: {
+ // based on https://node-postgres.com/apis/pool
+ // max connection pool size
+ max: this.configService.get('database.maxConnections', { infer: true }),
+ ssl: this.configService.get('database.sslEnabled', { infer: true })
+ ? {
+ rejectUnauthorized: this.configService.get(
+ 'database.rejectUnauthorized',
+ { infer: true },
+ ),
+ ca:
+ this.configService.get('database.ca', { infer: true }) ??
+ undefined,
+ key:
+ this.configService.get('database.key', { infer: true }) ??
+ undefined,
+ cert:
+ this.configService.get('database.cert', { infer: true }) ??
+ undefined,
+ }
+ : undefined,
+ },
+ } as TypeOrmModuleOptions;
+ }
+}
diff --git a/src/logger/logger.interceptor.ts b/src/logger/logger.interceptor.ts
new file mode 100644
index 000000000..4bf8a77a2
--- /dev/null
+++ b/src/logger/logger.interceptor.ts
@@ -0,0 +1,120 @@
+import {
+ Injectable,
+ NestInterceptor,
+ ExecutionContext,
+ CallHandler,
+} from '@nestjs/common';
+import { Observable } from 'rxjs';
+import { tap } from 'rxjs/operators';
+
+import { PinoLoggerService } from './logger.service';
+
+@Injectable()
+export class LoggingInterceptor implements NestInterceptor {
+ constructor(private readonly logger: PinoLoggerService) {}
+
+ intercept(context: ExecutionContext, next: CallHandler): Observable {
+ const request = context.switchToHttp().getRequest();
+ const { method, url, body, query, params, headers } = request;
+ const startTime = Date.now();
+
+ // One-line info log for incoming request
+ this.logger.log(`${method} ${url}`);
+
+ // Detailed debug log
+ this.logger.debug({
+ message: 'Incoming Request',
+ method,
+ url,
+ body,
+ query,
+ params,
+ headers: this.sanitizeHeaders(headers),
+ });
+
+ return next.handle().pipe(
+ tap({
+ next: (data) => {
+ const response = context.switchToHttp().getResponse();
+ const { statusCode } = response;
+ const duration = Date.now() - startTime;
+
+ // Log response
+ this.logger.debug({
+ message: 'Outgoing Response',
+ method,
+ url,
+ statusCode,
+ duration: `${duration}ms`,
+ response: this.sanitizeResponse(data),
+ });
+ },
+ error: (error) => {
+ const duration = Date.now() - startTime;
+ const statusCode = error.status || 500;
+
+ // Log error
+ this.logger.error({
+ message: 'Request Error',
+ method,
+ url,
+ statusCode,
+ duration: `${duration}ms`,
+ error: {
+ message: error.message,
+ stack: error.stack,
+ },
+ });
+ },
+ }),
+ );
+ }
+
+ private sanitizeHeaders(headers: Record): Record {
+ const sensitiveHeaders = ['authorization', 'cookie', 'set-cookie'];
+ const sanitized = { ...headers };
+
+ Object.keys(sanitized).forEach((key) => {
+ if (sensitiveHeaders.some((h) => h.toLowerCase() === key.toLowerCase())) {
+ sanitized[key] = '[REDACTED]';
+ }
+ });
+ return sanitized;
+ }
+
+ private sanitizeResponse(data: any): any {
+ if (!data) return data;
+
+ const sensitiveFields = [
+ 'password',
+ 'token',
+ 'refreshToken',
+ 'accessToken',
+ ];
+
+ if (Array.isArray(data)) {
+ return data.map((item) => this.sanitizeResponse(item));
+ }
+
+ if (typeof data === 'object' && data !== null) {
+ const sanitized = { ...data };
+
+ // Sanitize current level
+ sensitiveFields.forEach((field) => {
+ if (field in sanitized) {
+ sanitized[field] = '[REDACTED]';
+ }
+ });
+
+ // Recursively sanitize nested objects
+ for (const key in sanitized) {
+ if (typeof sanitized[key] === 'object' && sanitized[key] !== null) {
+ sanitized[key] = this.sanitizeResponse(sanitized[key]);
+ }
+ }
+
+ return sanitized;
+ }
+ return data;
+ }
+}
diff --git a/src/logger/logger.module.ts b/src/logger/logger.module.ts
new file mode 100644
index 000000000..b243b234a
--- /dev/null
+++ b/src/logger/logger.module.ts
@@ -0,0 +1,14 @@
+import { Global, Module } from '@nestjs/common';
+import { ConfigModule } from '@nestjs/config';
+
+import loggerConfig from '../config/logger.config';
+import { LoggingInterceptor } from './logger.interceptor';
+import { PinoLoggerService } from './logger.service';
+
+@Global()
+@Module({
+ imports: [ConfigModule.forFeature(loggerConfig)],
+ providers: [PinoLoggerService, LoggingInterceptor],
+ exports: [PinoLoggerService, LoggingInterceptor],
+})
+export class LoggerModule {}
diff --git a/src/logger/logger.service.ts b/src/logger/logger.service.ts
new file mode 100644
index 000000000..87ab71572
--- /dev/null
+++ b/src/logger/logger.service.ts
@@ -0,0 +1,51 @@
+import pino from 'pino';
+
+import { Injectable, LoggerService } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+
+import { AllConfigType } from '../config/config.type';
+
+@Injectable()
+export class PinoLoggerService implements LoggerService {
+ private logger: pino.Logger;
+
+ constructor(private configService: ConfigService) {
+ const loggerConfig = this.configService.get('logger', { infer: true });
+ if (!loggerConfig) {
+ throw new Error('Logger configuration is not defined');
+ }
+
+ this.logger = pino({
+ level: loggerConfig.level || 'info',
+ transport: loggerConfig.pretty
+ ? {
+ target: 'pino-pretty',
+ options: {
+ colorize: true,
+ translateTime: 'SYS:standard',
+ },
+ }
+ : undefined,
+ });
+ }
+
+ log(message: any, ...optionalParams: any[]) {
+ this.logger.info(message, ...optionalParams);
+ }
+
+ error(message: any, ...optionalParams: any[]) {
+ this.logger.error(message, ...optionalParams);
+ }
+
+ warn(message: any, ...optionalParams: any[]) {
+ this.logger.warn(message, ...optionalParams);
+ }
+
+ debug(message: any, ...optionalParams: any[]) {
+ this.logger.debug(message, ...optionalParams);
+ }
+
+ verbose(message: any, ...optionalParams: any[]) {
+ this.logger.trace(message, ...optionalParams);
+ }
+}
diff --git a/src/main.ts b/src/main.ts
index 481d8ed0a..7789ea4f8 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,59 +1,67 @@
-import 'dotenv/config';
-import {
- ClassSerializerInterceptor,
- ValidationPipe,
- VersioningType,
-} from '@nestjs/common';
-import { ConfigService } from '@nestjs/config';
-import { NestFactory, Reflector } from '@nestjs/core';
-import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
-import { useContainer } from 'class-validator';
-import { AppModule } from './app.module';
-import validationOptions from './utils/validation-options';
-import { AllConfigType } from './config/config.type';
-import { ResolvePromisesInterceptor } from './utils/serializer.interceptor';
-
-async function bootstrap() {
- const app = await NestFactory.create(AppModule, { cors: true });
- useContainer(app.select(AppModule), { fallbackOnErrors: true });
- const configService = app.get(ConfigService);
-
- app.enableShutdownHooks();
- app.setGlobalPrefix(
- configService.getOrThrow('app.apiPrefix', { infer: true }),
- {
- exclude: ['/'],
- },
- );
- app.enableVersioning({
- type: VersioningType.URI,
- });
- app.useGlobalPipes(new ValidationPipe(validationOptions));
- app.useGlobalInterceptors(
- // ResolvePromisesInterceptor is used to resolve promises in responses because class-transformer can't do it
- // https://github.com/typestack/class-transformer/issues/549
- new ResolvePromisesInterceptor(),
- new ClassSerializerInterceptor(app.get(Reflector)),
- );
-
- const options = new DocumentBuilder()
- .setTitle('API')
- .setDescription('API docs')
- .setVersion('1.0')
- .addBearerAuth()
- .addGlobalParameters({
- in: 'header',
- required: false,
- name: process.env.APP_HEADER_LANGUAGE || 'x-custom-lang',
- schema: {
- example: 'en',
- },
- })
- .build();
-
- const document = SwaggerModule.createDocument(app, options);
- SwaggerModule.setup('docs', app, document);
-
- await app.listen(configService.getOrThrow('app.port', { infer: true }));
-}
-void bootstrap();
+import 'dotenv/config';
+import {
+ ClassSerializerInterceptor,
+ ValidationPipe,
+ VersioningType,
+} from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import { NestFactory, Reflector } from '@nestjs/core';
+import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
+import { useContainer } from 'class-validator';
+import { AppModule } from './app.module';
+import validationOptions from './utils/validation-options';
+import { AllConfigType } from './config/config.type';
+import { ResolvePromisesInterceptor } from './utils/serializer.interceptor';
+import { LoggingInterceptor } from './logger/logger.interceptor';
+import { PinoLoggerService } from './logger/logger.service';
+
+async function bootstrap() {
+ const app = await NestFactory.create(AppModule, {
+ cors: true,
+ bufferLogs: true,
+ });
+ const logger = app.get(PinoLoggerService);
+ app.useLogger(logger);
+ useContainer(app.select(AppModule), { fallbackOnErrors: true });
+ const configService = app.get(ConfigService);
+
+ app.enableShutdownHooks();
+ app.setGlobalPrefix(
+ configService.getOrThrow('app.apiPrefix', { infer: true }),
+ {
+ exclude: ['/'],
+ },
+ );
+ app.enableVersioning({
+ type: VersioningType.URI,
+ });
+ app.useGlobalPipes(new ValidationPipe(validationOptions));
+ app.useGlobalInterceptors(
+ new LoggingInterceptor(logger),
+ new ResolvePromisesInterceptor(),
+ new ClassSerializerInterceptor(app.get(Reflector)),
+ );
+
+ const options = new DocumentBuilder()
+ .setTitle('API')
+ .setDescription('API docs')
+ .setVersion('1.0')
+ .addBearerAuth()
+ .addGlobalParameters({
+ in: 'header',
+ required: false,
+ name: process.env.APP_HEADER_LANGUAGE || 'x-custom-lang',
+ schema: {
+ example: 'en',
+ },
+ })
+ .build();
+
+ const document = SwaggerModule.createDocument(app, options);
+ SwaggerModule.setup('docs', app, document);
+
+ const port = configService.getOrThrow('app.port', { infer: true });
+ await app.listen(port);
+ logger.log(`Application is running on: http://localhost:${port}`);
+}
+void bootstrap();
From dc38ef5749c6dc2b62f188f89f878dd9ec5868bd Mon Sep 17 00:00:00 2001
From: Faraz Razi <72218210+FarazRazi@users.noreply.github.com>
Date: Mon, 21 Apr 2025 13:04:55 +0000
Subject: [PATCH 2/2] fix(eslint): added fix for endOfLine error due to windows
formatting
---
eslint.config.mjs | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 4bd58851c..2dba3d0e2 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,10 +1,11 @@
-import tsEslintPlugin from '@typescript-eslint/eslint-plugin';
import globals from 'globals';
-import tsParser from '@typescript-eslint/parser';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
-import js from '@eslint/js';
+
import { FlatCompat } from '@eslint/eslintrc';
+import js from '@eslint/js';
+import tsEslintPlugin from '@typescript-eslint/eslint-plugin';
+import tsParser from '@typescript-eslint/parser';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -60,6 +61,12 @@ export default [
message: '"it" should start with "should"',
},
],
+ 'prettier/prettier': [
+ 'error',
+ {
+ endOfLine: 'auto',
+ },
+ ],
},
},
];