From bbc06ae775717eaa39cad4930d9486d663f0b5a1 Mon Sep 17 00:00:00 2001 From: liuyun Date: Sat, 10 Oct 2026 21:31:05 +0800 Subject: [PATCH] feat: add pino structured logging (stdout + optional file via env) Co-Authored-By: Claude Opus 4.8 --- package.json | 3 + pnpm-lock.yaml | 145 ++++++++++++++++++++ src/app.module.ts | 2 + src/common/filters/http-exception.filter.ts | 11 +- src/logging/logger.config.ts | 28 ++++ src/main.ts | 4 +- 6 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 src/logging/logger.config.ts diff --git a/package.json b/package.json index 3991588..f10dafe 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,9 @@ "bcryptjs": "^3.0.3", "class-transformer": "^0.5.1", "class-validator": "^0.15.1", + "nestjs-pino": "^5.3.1", + "pino": "^10.4.0", + "pino-roll": "^4.0.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89e47bb..f9a90c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -190,6 +190,15 @@ importers: class-validator: specifier: ^0.15.1 version: 0.15.1 + nestjs-pino: + specifier: ^5.3.1 + version: 5.3.1(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(pino-http@11.0.0)(pino@10.4.0)(rxjs@7.8.2) + pino: + specifier: ^10.4.0 + version: 10.4.0 + pino-roll: + specifier: ^4.0.0 + version: 4.0.0 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -870,6 +879,9 @@ packages: '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@prisma/client@6.19.3': resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==} engines: {node: '>=18.18'} @@ -1164,6 +1176,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'} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -1363,6 +1379,9 @@ packages: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1561,6 +1580,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.7.0: resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==} engines: {node: '>=18'} @@ -1901,6 +1924,16 @@ packages: resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} engines: {node: '>=18'} + nestjs-pino@5.3.1: + resolution: {integrity: sha512-tL3fFSY38Wuv3VS4dCo+zlHVt1xU7+RSXnlgenyTvfy74GLBLgisqvG+76983ilf2C2uFkPe68qUapXCYnoSoA==} + engines: {node: '>=22.12.0'} + peerDependencies: + '@nestjs/common': ^11.0.8 || ^12.0.2 + '@nestjs/core': ^11.0.8 || ^12.0.2 + pino: ^10.0.0 + pino-http: ^11.0.0 + rxjs: ^7.1.0 + node-emoji@2.1.3: resolution: {integrity: sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==} engines: {node: '>=18'} @@ -1932,6 +1965,10 @@ packages: ohash@2.0.12: resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==} + 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'} @@ -1996,6 +2033,22 @@ packages: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} 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-roll@4.0.0: + resolution: {integrity: sha512-axI1aQaIxXdw1F4OFFli1EDxIrdYNGLowkw/ZoZogX8oCSLHUghzwVVXUS8U+xD/Savwa5IXpiXmsSGKFX/7Sg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.4.0: + resolution: {integrity: sha512-bk1ZMTwG/Vymx+zPoa28MhNzucFvXJWp3csrSuxuHPSMeUettaCoQ5Jvg07mtC4JGNpcfTwz41oVCnNgBK9+vA==} + hasBin: true + pkg-types@2.3.3: resolution: {integrity: sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==} @@ -2022,6 +2075,9 @@ packages: typescript: optional: true + process-warning@5.1.0: + resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} + proxy-addr@2.0.8: resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} engines: {node: '>= 0.10'} @@ -2033,6 +2089,9 @@ packages: resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + range-parser@1.3.0: resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} engines: {node: '>= 0.6'} @@ -2056,6 +2115,9 @@ packages: resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -2090,6 +2152,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==} @@ -2139,6 +2205,9 @@ packages: resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} engines: {node: '>=8'} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-js@1.2.2: resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} @@ -2154,6 +2223,10 @@ packages: resolution: {integrity: sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==} engines: {node: '>= 12'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -2211,6 +2284,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} + engines: {node: '>=20'} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -2867,6 +2944,8 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@pinojs/redact@0.4.0': {} + '@prisma/client@6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3)': optionalDependencies: prisma: 6.19.3(typescript@6.0.3) @@ -3142,6 +3221,8 @@ snapshots: asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + balanced-match@4.0.4: {} base64-js@1.5.1: {} @@ -3323,6 +3404,8 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + date-fns@4.4.0: {} + debug@4.4.3(supports-color@7.2.0): dependencies: ms: 2.1.3 @@ -3552,6 +3635,8 @@ snapshots: function-bind@1.1.2: {} + get-caller-file@2.0.5: {} + get-east-asian-width@1.7.0: {} get-intrinsic@1.3.0: @@ -3849,6 +3934,14 @@ snapshots: dependencies: content-type: 2.1.0 + nestjs-pino@5.3.1(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(pino-http@11.0.0)(pino@10.4.0)(rxjs@7.8.2): + dependencies: + '@nestjs/common': 12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + '@nestjs/core': 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.1.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + pino: 10.4.0 + pino-http: 11.0.0 + rxjs: 7.8.2 + node-emoji@2.1.3: dependencies: '@sindresorhus/is': 4.6.0 @@ -3879,6 +3972,8 @@ snapshots: ohash@2.0.12: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -3964,6 +4059,38 @@ snapshots: picomatch@4.0.7: {} + 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.4.0 + pino-std-serializers: 7.1.0 + process-warning: 5.1.0 + + pino-roll@4.0.0: + dependencies: + date-fns: 4.4.0 + sonic-boom: 4.2.1 + + pino-std-serializers@7.1.0: {} + + pino@10.4.0: + 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.1.0 + quick-format-unescaped: 4.0.4 + real-require: 1.0.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.2.0 + pkg-types@2.3.3: dependencies: confbox: 0.3.1 @@ -3989,6 +4116,8 @@ snapshots: transitivePeerDependencies: - magicast + process-warning@5.1.0: {} + proxy-addr@2.0.8: dependencies: forwarded: 0.2.0 @@ -4001,6 +4130,8 @@ snapshots: es-define-property: 1.0.1 side-channel: 1.1.1 + quick-format-unescaped@4.0.4: {} + range-parser@1.3.0: {} raw-body@3.0.2: @@ -4025,6 +4156,8 @@ snapshots: readdirp@5.1.1: {} + real-require@1.0.0: {} + reflect-metadata@0.2.2: {} require-from-string@2.0.2: {} @@ -4078,6 +4211,8 @@ snapshots: safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} semver@7.8.5: {} @@ -4147,6 +4282,10 @@ snapshots: dependencies: unicode-emoji-modifier-base: 1.0.0 + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.2: {} source-map-support@0.5.21: @@ -4158,6 +4297,8 @@ snapshots: source-map@0.8.0: {} + split2@4.2.0: {} + stackback@0.0.2: {} statuses@2.0.2: {} @@ -4223,6 +4364,10 @@ snapshots: dependencies: has-flag: 4.0.0 + thread-stream@4.2.0: + dependencies: + real-require: 1.0.0 + through@2.3.8: {} tinybench@2.9.0: {} diff --git a/src/app.module.ts b/src/app.module.ts index 2241a8c..c338b10 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { PrismaModule } from './prisma/prisma.module.js'; +import { createLoggerModule } from './logging/logger.config.js'; import { AuthModule } from './auth/auth.module.js'; import { JwtAuthGuard } from './auth/jwt-auth.guard.js'; import { PermissionsGuard } from './auth/permissions.guard.js'; @@ -21,6 +22,7 @@ import { AppService } from './app.service.js'; @Module({ imports: [ + createLoggerModule(), PrismaModule, AuthModule, TenantsModule, diff --git a/src/common/filters/http-exception.filter.ts b/src/common/filters/http-exception.filter.ts index 12263a1..7fa8deb 100644 --- a/src/common/filters/http-exception.filter.ts +++ b/src/common/filters/http-exception.filter.ts @@ -1,11 +1,14 @@ import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common'; import type { Response } from 'express'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; /** - * 全局异常过滤器:错误码分段与 HTTP 状态对齐(400xx/401xx/403xx/404xx/409xx/500xx)。 + * 全局异常过滤器:错误码分段与 HTTP 状态对齐(400xx/401xx/403xx/404xx/409xx/500xx),并记录日志。 */ @Catch() export class AllExceptionsFilter implements ExceptionFilter { + constructor(@InjectPinoLogger(AllExceptionsFilter.name) private readonly logger: PinoLogger) {} + catch(exception: unknown, host: ArgumentsHost): void { const ctx = host.switchToHttp(); const response = ctx.getResponse(); @@ -19,6 +22,12 @@ export class AllExceptionsFilter implements ExceptionFilter { ? ((body as { message: string[] }).message.join('; ')) : ((body as { message?: string })?.message ?? (exception as Error).message ?? 'Internal server error'); + if (status >= 500) { + this.logger.error({ err: exception }, message); + } else { + this.logger.warn(message); + } + response.status(status).json({ code: status * 100, message, diff --git a/src/logging/logger.config.ts b/src/logging/logger.config.ts new file mode 100644 index 0000000..36b50ac --- /dev/null +++ b/src/logging/logger.config.ts @@ -0,0 +1,28 @@ +import { DynamicModule } from '@nestjs/common'; +import { LoggerModule } from 'nestjs-pino'; + +/** + * 日志配置:pino 结构化日志。 + * - stdout 默认开启 + * - 文件输出默认关闭,由环境变量 LOG_FILE_ENABLED=true 开启 + * - 级别 LOG_LEVEL(默认 info)、路径 LOG_FILE_PATH(默认 logs/app.log) + */ +export function createLoggerModule(): DynamicModule { + const level = process.env.LOG_LEVEL ?? 'info'; + const fileEnabled = process.env.LOG_FILE_ENABLED === 'true'; + const filePath = process.env.LOG_FILE_PATH ?? 'logs/app.log'; + + const pinoHttp = fileEnabled + ? { + level, + transport: { + targets: [ + { target: 'pino/file', options: { destination: 1 } }, // stdout + { target: 'pino-roll', options: { file: filePath, size: '10m', limit: { count: 10 } } }, // 文件按大小轮转 + ], + }, + } + : { level }; + + return LoggerModule.forRoot({ pinoHttp }); +} diff --git a/src/main.ts b/src/main.ts index eda39ca..e8ee779 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,15 @@ import 'dotenv/config'; import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; +import { Logger } from 'nestjs-pino'; import { AppModule } from './app.module.js'; async function bootstrap(): Promise { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { bufferLogs: true }); app.setGlobalPrefix('api/v1'); app.enableCors(); app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); + app.useLogger(app.get(Logger)); await app.listen(process.env.PORT ?? 3000); } await bootstrap();