diff --git a/docs/adr/0015-structured-logging-with-pino.md b/docs/adr/0015-structured-logging-with-pino.md new file mode 100644 index 0000000..0b162fc --- /dev/null +++ b/docs/adr/0015-structured-logging-with-pino.md @@ -0,0 +1,37 @@ +# ADR 0015: Structured Logging with Pino + +## Context + +The application needs production-ready observability. Up to this point, standard `console.log` and `console.error` methods (either directly or via generic NestJS Logger wrapping them natively) were being used. + +However, `console.log` presents several issues for a production environment: + +1. **Lack of Structure:** Logs are emitted as raw strings, making them difficult to parse consistently by log aggregators (e.g., Datadog, CloudWatch, ELK). +2. **Blocking Operations:** Native Node.js console methods are synchronous and blocking under certain conditions, which limits throughput and degrades application performance under high load. +3. **Security and Privacy Risks:** Without an automated redaction system, critical PII (Personally Identifiable Information) such as `password`, `email`, or `refreshToken` can accidentally be leaked into log sinks, causing severe compliance and security violations. + +A robust log infrastructure needs to enforce structure, improve performance, and protect sensitive data gracefully. + +## Decision + +We will use [`nestjs-pino`](https://github.com/iamolegga/nestjs-pino) and underlying [`pino-http`](https://github.com/pinojs/pino-http) for global application logging. + +### Justification + +We chose Pino over other logging utilities like Winston or Morgan because: +1. **Performance/Throughput:** Pino has the lowest overhead in the Node.js ecosystem, using asynchronous fast-serialization strategies that minimize performance impact. +2. **Native JSON Formatting:** Pino natively outputs logs in newline-delimited JSON format (`ndjson`), which is explicitly required for easy and reliable ingestion into modern cloud aggregators. +3. **Built-in Redaction:** Pino has an extremely high-performance built-in data masking/redaction system, allowing us to specify paths (like `req.headers.authorization` or `req.body.password`) that handles PII omission out of the box. +4. **Developer Experience:** While JSON is great for machines, it's hard to read during development. Pino integrates neatly with `pino-pretty` to output formatted, colored logs in `dev` or non-production environments without modifying the underlying logging mechanics. + +## Consequences + +### Positive + +* **High Observability:** A generated correlation ID allows requests to be traced logically throughout their entire lifecycle. +* **Privacy Compliance by Default:** The global logger configuration automatically intercepts and masks authentication, authorization hooks, passwords, and sensitive keys. +* **Aggregator Ready:** Directly fits with modern telemetry collectors without needing complex grok parsing logic. + +### Negative + +* **Strict Tooling Dependency:** `nestjs-pino` wraps NestJS' logger interface tightly. Teams must be mindful of using the injected `Logger` and avoiding global standard `console.*` scopes, as those will bypass the Pino formatter and correlation ID generator. diff --git a/docs/adr/README.md b/docs/adr/README.md index 9a3ba5b..48ed606 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -41,6 +41,7 @@ The decisions described here **reflect the actual state of the code at the time | 0012 | Entity Factory Method Separation | | 0013 | Stateless JWT Authentication | | 0014 | Refresh Token Strategy | +| 0015 | Structured Logging with Pino | diff --git a/jest.config.ts b/jest.config.ts index 865f579..fe5461c 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -11,6 +11,7 @@ const config: Config = { }, preset: 'ts-jest', testEnvironment: 'node', + setupFilesAfterEnv: ['/test/setup-after-env.ts'], }; export default config; diff --git a/package.json b/package.json index 8481975..03d6fa5 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,12 @@ "cookie-parser": "^1.4.7", "drizzle-orm": "^0.45.1", "helmet": "^8.1.0", + "nestjs-pino": "^4.6.1", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "pg": "^8.16.3", + "pino-http": "^11.0.0", + "pino-pretty": "^13.1.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c7c3e4..38c66bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: helmet: specifier: ^8.1.0 version: 8.1.0 + nestjs-pino: + specifier: ^4.6.1 + version: 4.6.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.3.1)(rxjs@7.8.2) passport: specifier: ^0.7.0 version: 0.7.0 @@ -59,6 +62,12 @@ importers: pg: specifier: ^8.16.3 version: 8.19.0 + pino-http: + specifier: ^11.0.0 + version: 11.0.0 + pino-pretty: + specifier: ^13.1.3 + version: 13.1.3 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -1142,6 +1151,9 @@ packages: resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} engines: {node: '>=10'} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1633,6 +1645,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + babel-jest@30.2.0: resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1816,6 +1832,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -1902,6 +1921,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2098,6 +2120,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.19.0: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} @@ -2292,6 +2317,9 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + fast-copy@4.0.3: + resolution: {integrity: sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2485,6 +2513,9 @@ packages: resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==} engines: {node: '>=18.0.0'} + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2739,6 +2770,10 @@ packages: node-notifier: optional: true + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2985,6 +3020,15 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + nestjs-pino@4.6.1: + resolution: {integrity: sha512-nuARXa0xpdJ1lY2+fgycIQr6H3g0VgqAWNK3xMYjOFcj2DoPETNXj0lV3Y86nRuI7BUfQp5PGiVoZvT4dTWbpQ==} + engines: {node: '>= 14'} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + pino: ^7.5.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + pino-http: ^6.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + rxjs: ^7.1.0 + node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} @@ -3021,6 +3065,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -3168,6 +3216,23 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-http@11.0.0: + resolution: {integrity: sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==} + + pino-pretty@13.1.3: + resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} + hasBin: true + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -3213,10 +3278,16 @@ packages: resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3228,6 +3299,9 @@ packages: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -3250,6 +3324,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -3298,6 +3376,10 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -3309,6 +3391,9 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -3367,6 +3452,9 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -3443,6 +3531,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} @@ -3507,6 +3599,10 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -4801,6 +4897,8 @@ snapshots: '@phc/format@1.0.0': {} + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -5325,6 +5423,8 @@ snapshots: asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + babel-jest@30.2.0(@babel/core@7.29.0): dependencies: '@babel/core': 7.29.0 @@ -5530,6 +5630,8 @@ snapshots: color-name@1.1.4: {} + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -5605,6 +5707,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + dateformat@4.6.3: {} + debug@3.2.7: dependencies: ms: 2.1.3 @@ -5684,6 +5788,10 @@ snapshots: encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 @@ -5966,6 +6074,8 @@ snapshots: transitivePeerDependencies: - supports-color + fast-copy@4.0.3: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -6177,6 +6287,8 @@ snapshots: helmet@8.1.0: {} + help-me@5.0.0: {} + html-escaper@2.0.2: {} http-errors@2.0.1: @@ -6606,6 +6718,8 @@ snapshots: - supports-color - ts-node + joycon@3.1.1: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -6816,6 +6930,13 @@ snapshots: neo-async@2.6.2: {} + nestjs-pino@4.6.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.3.1)(rxjs@7.8.2): + dependencies: + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + pino: 10.3.1 + pino-http: 11.0.0 + rxjs: 7.8.2 + node-abort-controller@3.1.1: {} node-addon-api@8.5.0: {} @@ -6840,6 +6961,8 @@ snapshots: object-inspect@1.13.4: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -6986,6 +7109,49 @@ snapshots: picomatch@4.0.3: {} + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-http@11.0.0: + dependencies: + get-caller-file: 2.0.5 + pino: 10.3.1 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + + pino-pretty@13.1.3: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 4.0.3 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pump: 3.0.4 + secure-json-parse: 4.1.0 + sonic-boom: 4.2.1 + strip-json-comments: 5.0.3 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.0.0 + pirates@4.0.7: {} pkg-dir@4.2.0: @@ -7018,11 +7184,18 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + process-warning@5.0.0: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} pure-rand@7.0.1: {} @@ -7031,6 +7204,8 @@ snapshots: dependencies: side-channel: 1.1.0 + quick-format-unescaped@4.0.4: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -7054,6 +7229,8 @@ snapshots: readdirp@4.1.2: {} + real-require@0.2.0: {} + reflect-metadata@0.2.2: {} require-directory@2.1.1: {} @@ -7101,6 +7278,8 @@ snapshots: safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} schema-utils@3.3.0: @@ -7116,6 +7295,8 @@ snapshots: ajv-formats: 2.1.1(ajv@8.18.0) ajv-keywords: 5.1.0(ajv@8.18.0) + secure-json-parse@4.1.0: {} + semver@6.3.1: {} semver@7.7.4: {} @@ -7191,6 +7372,10 @@ snapshots: slash@3.0.0: {} + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-support@0.5.13: dependencies: buffer-from: 1.1.2 @@ -7256,6 +7441,8 @@ snapshots: strip-json-comments@3.1.1: {} + strip-json-comments@5.0.3: {} + strtok3@10.3.4: dependencies: '@tokenizer/token': 0.3.0 @@ -7328,6 +7515,10 @@ snapshots: glob: 7.2.3 minimatch: 3.1.5 + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) diff --git a/src/app.module.ts b/src/app.module.ts index e65d4b4..dfc3afc 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,9 +4,12 @@ import { InfrastructureModule } from '@infrastructure/infrastructure.module'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { AppLoggerModule } from './infrastructure/logging/logger.module'; + @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), + AppLoggerModule, ApplicationModule, InfrastructureModule, HttpModule, diff --git a/src/infrastructure/logging/logger.module.ts b/src/infrastructure/logging/logger.module.ts new file mode 100644 index 0000000..7034e9c --- /dev/null +++ b/src/infrastructure/logging/logger.module.ts @@ -0,0 +1,56 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { randomUUID } from 'crypto'; +import { IncomingMessage } from 'http'; +import { LoggerModule } from 'nestjs-pino'; + +@Module({ + imports: [ + LoggerModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => { + const isProduction = config.get('NODE_ENV') === 'production'; + const isTest = config.get('NODE_ENV') === 'test'; + + return { + pinoHttp: { + // Correlation ID: Ensure each request gets a unique ID + genReqId: (req: IncomingMessage) => { + return req.headers['x-correlation-id'] || randomUUID(); + }, + // PII Masking: auto mask sensitive fields + redact: { + paths: [ + 'req.headers.authorization', + 'req.body.password', + 'req.body.passwordHash', + 'req.body.email', + 'req.body.refreshToken', + 'password', + 'passwordHash', + 'email', + 'refreshToken', + ], + censor: '***', + }, + // Pretty Print ONLY when not in production and NOT in test + transport: + !isProduction && !isTest + ? { + target: 'pino-pretty', + options: { + singleLine: true, + colorize: true, + }, + } + : undefined, + // JSON format is default in pino for production and tests + }, + }; + }, + }), + ], + exports: [LoggerModule], +}) +export class AppLoggerModule {} diff --git a/src/interfaces/http/filters/global-exception.filter.ts b/src/interfaces/http/filters/global-exception.filter.ts index 027b7c6..a57dc0d 100644 --- a/src/interfaces/http/filters/global-exception.filter.ts +++ b/src/interfaces/http/filters/global-exception.filter.ts @@ -6,15 +6,15 @@ import { ExceptionFilter, HttpException, HttpStatus, - Logger, } from '@nestjs/common'; import { Response } from 'express'; +import { Logger } from 'nestjs-pino'; import { DomainErrorHttpMapper } from '../errors/domain-error-http.mapper'; @Catch() export class GlobalExceptionFilter implements ExceptionFilter { - private readonly logger = new Logger(GlobalExceptionFilter.name); + constructor(private readonly logger: Logger) {} catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); diff --git a/src/main.ts b/src/main.ts index e2393ae..282c8be 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,6 @@ import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { Logger } from 'nestjs-pino'; import { configureApp } from './app.config'; import { AppModule } from './app.module'; @@ -7,6 +8,9 @@ import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); + const logger = app.get(Logger); + app.useLogger(logger); + configureApp(app); const config = new DocumentBuilder()