From 8b9c84ceabb5b231adaf165000ed100915d59922 Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 8 May 2026 01:36:30 +0300 Subject: [PATCH 1/6] feat: add winston logger and add to bootstrap setups --- libs/bootstrap/src/bootstrap.ts | 16 +- libs/bootstrap/src/setups/index.ts | 1 + libs/bootstrap/src/setups/logger.ts | 35 +++++ package.json | 4 + pnpm-lock.yaml | 220 ++++++++++++++++++++++++++++ src/shared/error/filter.ts | 54 +++++++ 6 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 libs/bootstrap/src/setups/logger.ts diff --git a/libs/bootstrap/src/bootstrap.ts b/libs/bootstrap/src/bootstrap.ts index 3a88ff9..c06e560 100644 --- a/libs/bootstrap/src/bootstrap.ts +++ b/libs/bootstrap/src/bootstrap.ts @@ -3,7 +3,7 @@ import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; import { setupThrottler } from './setups/throttler'; import { DEFAULT_THROTTLER_OPTIONS } from './configs/throttler'; -import { setupCors, setupSwagger } from './setups'; +import { setupCors, setupLogger, setupSwagger } from './setups'; import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; import type { BootstrapOptions } from './interfaces/options.interface'; import fastifyCookie from '@fastify/cookie'; @@ -16,11 +16,14 @@ export async function bootstrapApp(options: BootstrapOptions) { const startTime = performance.now(); const adapter = new FastifyAdapter({ requestIdHeader: 'x-request-id', + requestIdLogLabel: 'request', genReqId: (req) => { return (req.headers['x-request-id'] as string) || createId(); }, }); + const winston = setupLogger(options.serviceName); + const { appModule, apiPrefix, @@ -43,7 +46,11 @@ export async function bootstrapApp(options: BootstrapOptions) { const app = await NestFactory.create(rootModule, adapter, { rawBody: true, + bufferLogs: true, }); + + app.useLogger(winston); + const logger = new Logger(serviceName[0].toUpperCase() + serviceName.slice(1)); const configService = app.get(ConfigService); const port = configService.getOrThrow(portEnvKey, defaultPort); @@ -51,6 +58,13 @@ export async function bootstrapApp(options: BootstrapOptions) { app.enableShutdownHooks(); + app.getHttpAdapter() + .getInstance() + .addHook('onSend', async (request, reply, payload) => { + reply.header('x-request-id', request.id); + return payload; + }); + await app.register(fastifyCompress, { global: true, threshold: 1024, diff --git a/libs/bootstrap/src/setups/index.ts b/libs/bootstrap/src/setups/index.ts index 2cfe699..e5d3f07 100644 --- a/libs/bootstrap/src/setups/index.ts +++ b/libs/bootstrap/src/setups/index.ts @@ -1,3 +1,4 @@ export { setupCors } from './cors'; export { setupThrottler } from './throttler'; export { setupSwagger } from './swagger'; +export { setupLogger } from './logger'; diff --git a/libs/bootstrap/src/setups/logger.ts b/libs/bootstrap/src/setups/logger.ts new file mode 100644 index 0000000..604b948 --- /dev/null +++ b/libs/bootstrap/src/setups/logger.ts @@ -0,0 +1,35 @@ +import { WinstonModule, utilities } from 'nest-winston'; +import { format, transports } from 'winston'; + +export function setupLogger(service: string) { + const isProduction = process.env.NODE_ENV === 'production'; + + return WinstonModule.createLogger({ + level: isProduction ? 'info' : 'debug', + transports: [ + new transports.Console({ + format: format.combine( + format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + format.ms(), + format.errors({ stack: true }), + format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'context'] }), + format((info) => { + if (!isProduction) { + // @ts-expect-error : Will resolved + const rid = info.metadata?.requestId; + info.message = rid ? `[${rid}] ${info.message}` : info.message; + } + return info; + })(), + + isProduction + ? format.json() + : utilities.format.nestLike(service, { + colors: true, + prettyPrint: false, + }), + ), + }), + ], + }); +} diff --git a/package.json b/package.json index 118c4ac..ea112c6 100644 --- a/package.json +++ b/package.json @@ -60,12 +60,14 @@ "argon2": "^0.44.0", "axios": "^1.16.0", "bullmq": "^5.73.4", + "cls-rtracer": "^2.6.3", "dotenv": "^17.4.2", "drizzle-orm": "^0.45.2", "drizzle-zod": "^0.8.3", "fastify": "^5.8.4", "handlebars": "^4.7.9", "ioredis": "^5.10.1", + "nest-winston": "^1.10.2", "nestjs-zod": "^5.3.0", "nodemailer": "^8.0.5", "otplib": "^13.4.0", @@ -76,6 +78,8 @@ "rxjs": "^7.8.1", "transliteration": "^2.6.1", "ua-parser-js": "^2.0.9", + "winston": "^3.19.0", + "winston-daily-rotate-file": "^5.0.0", "zod": "^4.3.6" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce40199..9b07dd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: bullmq: specifier: ^5.73.4 version: 5.73.4 + cls-rtracer: + specifier: ^2.6.3 + version: 2.6.3 dotenv: specifier: ^17.4.2 version: 17.4.2 @@ -110,6 +113,9 @@ importers: ioredis: specifier: ^5.10.1 version: 5.10.1 + nest-winston: + specifier: ^1.10.2 + version: 1.10.2(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(winston@3.19.0) nestjs-zod: specifier: ^5.3.0 version: 5.3.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) @@ -140,6 +146,12 @@ importers: ua-parser-js: specifier: ^2.0.9 version: 2.0.9 + winston: + specifier: ^3.19.0 + version: 3.19.0 + winston-daily-rotate-file: + specifier: ^5.0.0 + version: 5.0.0(winston@3.19.0) zod: specifier: ^4.3.6 version: 4.3.6 @@ -466,6 +478,10 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + '@commitlint/cli@20.5.0': resolution: {integrity: sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==} engines: {node: '>=v18'} @@ -547,6 +563,9 @@ packages: conventional-commits-parser: optional: true + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} @@ -1902,6 +1921,9 @@ packages: resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} engines: {node: '>=18.0.0'} + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -2080,6 +2102,9 @@ packages: '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/ua-parser-js@0.7.39': resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==} @@ -2504,6 +2529,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cls-rtracer@2.6.3: + resolution: {integrity: sha512-O7M/m2M/KfT9v+q7ka9nmsadS67ce9P8+1Zgm6VFamK56oFd1iCoJ9m8hYKUQpK4+RofyaexxHJlOBkxqCDs3Q==} + engines: {node: '>=12.17.0 <13.0.0 || >=13.14.0 <14.0.0 || >=14.0.0'} + cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} @@ -2512,9 +2541,25 @@ packages: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} + + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -2812,6 +2857,9 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -2999,10 +3047,16 @@ packages: picomatch: optional: true + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} + file-stream-rotator@0.6.1: + resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} + file-type@21.3.4: resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} engines: {node: '>=20'} @@ -3029,6 +3083,9 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} @@ -3248,6 +3305,10 @@ packages: is-standalone-pwa@0.1.1: resolution: {integrity: sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -3339,6 +3400,9 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -3503,6 +3567,10 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + lru-cache@11.3.3: resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==} engines: {node: 20 || >=22} @@ -3590,6 +3658,9 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3615,6 +3686,12 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + nest-winston@1.10.2: + resolution: {integrity: sha512-Z9IzL/nekBOF/TEwBHUJDiDPMaXUcFquUQOFavIRet6xF0EbuWnOzslyN/ksgzG+fITNgXhMdrL/POp9SdaFxA==} + peerDependencies: + '@nestjs/common': ^5.0.0 || ^6.6.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + winston: ^3.0.0 + nestjs-zod@5.3.0: resolution: {integrity: sha512-QY6imXm9heMOpWigjFHgMWPvc1ZQHeNQ7pdogo9Q5xj5F8HpqZ972vKlVdkaTyzYlOXJP/yVy3wlF1EjubDQPg==} peerDependencies: @@ -3651,6 +3728,10 @@ packages: resolution: {integrity: sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==} engines: {node: '>=6.0.0'} + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -3661,6 +3742,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -4053,6 +4137,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -4157,6 +4244,9 @@ packages: engines: {node: '>=10'} hasBin: true + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -4203,6 +4293,10 @@ packages: engines: {node: '>=20.0.0'} hasBin: true + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -4292,6 +4386,11 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + vite@8.0.8: resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4415,6 +4514,20 @@ packages: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} + winston-daily-rotate-file@5.0.0: + resolution: {integrity: sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==} + engines: {node: '>=8'} + peerDependencies: + winston: ^3 + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} + engines: {node: '>= 12.0.0'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -5039,6 +5152,8 @@ snapshots: '@colors/colors@1.5.0': optional: true + '@colors/colors@1.6.0': {} + '@commitlint/cli@20.5.0(@types/node@20.19.39)(conventional-commits-parser@6.4.0)(typescript@5.9.3)': dependencies: '@commitlint/format': 20.5.0 @@ -5161,6 +5276,12 @@ snapshots: optionalDependencies: conventional-commits-parser: 6.4.0 + '@dabh/diagnostics@2.0.8': + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + '@drizzle-team/brocli@0.10.2': {} '@emnapi/core@1.9.2': @@ -6379,6 +6500,11 @@ snapshots: dependencies: tslib: 2.8.1 + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + '@standard-schema/spec@1.1.0': {} '@swc/core-darwin-arm64@1.15.24': @@ -6553,6 +6679,8 @@ snapshots: '@types/http-errors': 2.0.5 '@types/node': 20.19.39 + '@types/triple-beam@1.3.5': {} + '@types/ua-parser-js@0.7.39': {} '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': @@ -7042,14 +7170,33 @@ snapshots: clone@1.0.4: {} + cls-rtracer@2.6.3: + dependencies: + uuid: 9.0.1 + cluster-key-slot@1.1.2: {} color-convert@2.0.1: dependencies: color-name: 1.1.4 + color-convert@3.1.3: + dependencies: + color-name: 2.1.0 + color-name@1.1.4: {} + color-name@2.1.0: {} + + color-string@2.1.4: + dependencies: + color-name: 2.1.0 + + color@5.0.3: + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + colorette@2.0.20: {} combined-stream@1.0.8: @@ -7235,6 +7382,8 @@ snapshots: emoji-regex@8.0.0: {} + enabled@2.0.0: {} + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -7522,10 +7671,16 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fecha@4.2.3: {} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 + file-stream-rotator@0.6.1: + dependencies: + moment: 2.30.1 + file-type@21.3.4: dependencies: '@tokenizer/inflate': 0.4.1 @@ -7562,6 +7717,8 @@ snapshots: flatted@3.4.2: {} + fn.name@1.1.0: {} + follow-redirects@1.16.0: {} fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7)): @@ -7787,6 +7944,8 @@ snapshots: is-standalone-pwa@0.1.1: {} + is-stream@2.0.1: {} + is-unicode-supported@0.1.0: {} isarray@1.0.0: {} @@ -7882,6 +8041,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kuler@2.0.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -8017,6 +8178,15 @@ snapshots: strip-ansi: 7.2.0 wrap-ansi: 9.0.2 + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + lru-cache@11.3.3: {} luxon@3.7.2: {} @@ -8088,6 +8258,8 @@ snapshots: minipass@7.1.3: {} + moment@2.30.1: {} + ms@2.1.3: {} msgpackr-extract@3.0.3: @@ -8114,6 +8286,12 @@ snapshots: neo-async@2.6.2: {} + nest-winston@1.10.2(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(winston@3.19.0): + dependencies: + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + fast-safe-stringify: 2.1.1 + winston: 3.19.0 + nestjs-zod@5.3.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6): dependencies: '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -8142,6 +8320,8 @@ snapshots: nodemailer@8.0.5: {} + object-hash@3.0.0: {} + obug@2.1.1: {} on-exit-leak-free@2.1.2: {} @@ -8150,6 +8330,10 @@ snapshots: dependencies: wrappy: 1.0.2 + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -8541,6 +8725,8 @@ snapshots: split2@4.2.0: {} + stack-trace@0.0.10: {} + stackback@0.0.2: {} standard-as-callback@2.1.0: {} @@ -8634,6 +8820,8 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + text-hex@1.0.0: {} + text-table@0.2.0: {} thread-stream@4.0.0: @@ -8672,6 +8860,8 @@ snapshots: transliteration@2.6.1: {} + triple-beam@1.4.1: {} + ts-api-utils@1.4.3(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -8753,6 +8943,8 @@ snapshots: uuid@11.1.0: {} + uuid@9.0.1: {} + vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 @@ -8857,6 +9049,34 @@ snapshots: string-width: 4.2.3 optional: true + winston-daily-rotate-file@5.0.0(winston@3.19.0): + dependencies: + file-stream-rotator: 0.6.1 + object-hash: 3.0.0 + triple-beam: 1.4.1 + winston: 3.19.0 + winston-transport: 4.9.0 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.19.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + word-wrap@1.2.5: {} wordwrap@1.0.0: {} diff --git a/src/shared/error/filter.ts b/src/shared/error/filter.ts index 1d6724b..efd4c11 100644 --- a/src/shared/error/filter.ts +++ b/src/shared/error/filter.ts @@ -4,6 +4,7 @@ import { ExceptionFilter, HttpException, HttpStatus, + Logger, } from '@nestjs/common'; import { ZodValidationException } from 'nestjs-zod'; import type { FastifyReply, FastifyRequest } from 'fastify'; @@ -15,6 +16,7 @@ import { DATABASE_ERRORS } from './swagger'; @Catch() export class GlobalExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(GlobalExceptionFilter.name); private isDev = process.env.NODE_ENV === 'development'; catch(exception: unknown, host: ArgumentsHost) { @@ -44,6 +46,11 @@ export class GlobalExceptionFilter implements ExceptionFilter { const zodError = exception.getZodError() as ZodError; const issues: ZodIssue[] = zodError.issues || []; + this.log(exception, host, status, { + validationIssues: issues, + body: request.body, + }); + return response.status(status).send( this.formatErrorResponse(request, status, { code: 'VALIDATION_FAILED', @@ -76,6 +83,13 @@ export class GlobalExceptionFilter implements ExceptionFilter { } } + this.log(exception, host, status, { + dbCode: error?.code, + dbTable: error?.table, + dbDetail: error?.detail, + query: this.isDev ? exception.message : undefined, + }); + return response.status(status).send( this.formatErrorResponse(request, status, { code: errorCode, @@ -93,6 +107,12 @@ export class GlobalExceptionFilter implements ExceptionFilter { const error = exception.getResponse() as IErrorOptions; + this.log(exception, host, status, { + errorCode: error.code, + details: error.details, + type: 'BUSINESS_EXCEPTION', + }); + return response.status(status).send( this.formatErrorResponse(request, status, { code: error.code, @@ -116,6 +136,12 @@ export class GlobalExceptionFilter implements ExceptionFilter { ? res['error'].toUpperCase().replace(/\s+/g, '_') : 'HTTP_EXCEPTION'; + this.log(exception, host, status, { + httpCode: code, + nestResponse: res, + type: 'NEST_HTTP_EXCEPTION', + }); + return response.status(status).send( this.formatErrorResponse(request, status, { code, @@ -180,4 +206,32 @@ export class GlobalExceptionFilter implements ExceptionFilter { request: ctx.getRequest(), }; } + + private log( + exception: any, + host: ArgumentsHost, + status: number, + extraData: Record = {}, + ) { + const { request } = this.getCtxBase(host); + + const logContext = { + status, + path: request.url, + method: request.method, + requestId: request.id ?? request.headers['x-request-id'], + ...extraData, + ...(status >= 500 && { + stack: exception instanceof Error ? exception.stack : exception, + }), + }; + + const message = `[${status}] ${request.method} ${request.url} - ${exception?.message || 'Unknown Error'}`; + + if (status >= 500) { + this.logger.error(message, logContext); + } else { + this.logger.warn(message, logContext); + } + } } From 0aa00d372d3cb8f4aff8b270a9a81be252b47b75 Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 8 May 2026 16:54:45 +0300 Subject: [PATCH 2/6] feat(bootstrap): implement global logging interceptor and exception filters --- libs/bootstrap/src/bootstrap.ts | 5 +- libs/bootstrap/src/setups/index.ts | 2 +- libs/bootstrap/src/setups/logger.ts | 88 +++++++++++++++++-- .../src/controller/health.controller.ts | 5 +- src/shared/error/filter.ts | 19 ++-- src/shared/media/workers/media.worker.ts | 5 -- .../listeners/update-media.listener.ts | 10 +-- .../listeners/update-avatar.listener.ts | 12 +-- 8 files changed, 104 insertions(+), 42 deletions(-) diff --git a/libs/bootstrap/src/bootstrap.ts b/libs/bootstrap/src/bootstrap.ts index c06e560..25d56d6 100644 --- a/libs/bootstrap/src/bootstrap.ts +++ b/libs/bootstrap/src/bootstrap.ts @@ -1,9 +1,8 @@ import { Logger, VersioningType } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; -import { setupThrottler } from './setups/throttler'; import { DEFAULT_THROTTLER_OPTIONS } from './configs/throttler'; -import { setupCors, setupLogger, setupSwagger } from './setups'; +import { setupCors, setupLogger, setupThrottler, setupSwagger, LoggingInterceptor } from './setups'; import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; import type { BootstrapOptions } from './interfaces/options.interface'; import fastifyCookie from '@fastify/cookie'; @@ -65,6 +64,8 @@ export async function bootstrapApp(options: BootstrapOptions) { return payload; }); + app.useGlobalInterceptors(new LoggingInterceptor()); + await app.register(fastifyCompress, { global: true, threshold: 1024, diff --git a/libs/bootstrap/src/setups/index.ts b/libs/bootstrap/src/setups/index.ts index e5d3f07..e066846 100644 --- a/libs/bootstrap/src/setups/index.ts +++ b/libs/bootstrap/src/setups/index.ts @@ -1,4 +1,4 @@ export { setupCors } from './cors'; export { setupThrottler } from './throttler'; export { setupSwagger } from './swagger'; -export { setupLogger } from './logger'; +export { setupLogger, LoggingInterceptor } from './logger'; diff --git a/libs/bootstrap/src/setups/logger.ts b/libs/bootstrap/src/setups/logger.ts index 604b948..2ce6b19 100644 --- a/libs/bootstrap/src/setups/logger.ts +++ b/libs/bootstrap/src/setups/logger.ts @@ -1,3 +1,13 @@ +import { + Injectable, + NestInterceptor, + type ExecutionContext, + type CallHandler, + Logger, +} from '@nestjs/common'; +import { Observable, throwError } from 'rxjs'; +import { tap, catchError } from 'rxjs/operators'; +import type { FastifyRequest } from 'fastify'; import { WinstonModule, utilities } from 'nest-winston'; import { format, transports } from 'winston'; @@ -14,11 +24,14 @@ export function setupLogger(service: string) { format.errors({ stack: true }), format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'context'] }), format((info) => { - if (!isProduction) { - // @ts-expect-error : Will resolved - const rid = info.metadata?.requestId; - info.message = rid ? `[${rid}] ${info.message}` : info.message; - } + const mask = (obj: any) => { + const sensitive = ['password', 'token', 'secret', 'authorization']; + for (const key in obj) { + if (sensitive.includes(key.toLowerCase())) obj[key] = '***'; + else if (typeof obj[key] === 'object') mask(obj[key]); + } + }; + if (info.metadata) mask(info.metadata); return info; })(), @@ -33,3 +46,68 @@ export function setupLogger(service: string) { ], }); } + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + private readonly logger = new Logger('HTTP'); + private readonly sensitiveFields = ['password', 'token', 'access', 'refresh', 'code', 'secret']; + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + const { method, url, body, query, ip, headers } = request; + + const requestId = request.id || request.headers['x-request-id'] || 'unknown'; + const userAgent = headers['user-agent'] || 'unknown'; + const startTime = Date.now(); + + const sanitizedBody = this.sanitize(body); + const queryPart = Object.keys(query).length ? `| Query: ${JSON.stringify(query)} ` : ''; + const bodyPart = + sanitizedBody && Object.keys(sanitizedBody).length + ? `| Body: ${JSON.stringify(sanitizedBody)} ` + : ''; + + this.logger.log( + `[${method}][${requestId}] ${url} ${queryPart}${bodyPart}| IP: ${ip} | UA: ${userAgent}`, + ); + + return next.handle().pipe( + tap(() => { + const delay = Date.now() - startTime; + + this.logger.log(`[${method}][${requestId}] ${url} | Success | ${delay}ms`); + }), + catchError((err) => { + const delay = Date.now() - startTime; + const statusCode = err.status || err.statusCode || 500; + + this.logger.error( + `[${method}][${requestId}] ${url} | Status: ${statusCode} | ${delay}ms | Msg: ${err.message}`, + err.stack, + ); + + return throwError(() => err); + }), + ); + } + + private sanitize(data: any): any { + if (!data || typeof data !== 'object') return data; + if (Array.isArray(data)) return data.map((v) => this.sanitize(v)); + + return Object.keys(data).reduce((acc, key) => { + const isSensitive = this.sensitiveFields.some((field) => + key.toLowerCase().includes(field), + ); + + if (isSensitive) { + acc[key] = '***'; + } else if (typeof data[key] === 'object') { + acc[key] = this.sanitize(data[key]); + } else { + acc[key] = data[key]; + } + return acc; + }, {}); + } +} diff --git a/libs/health/src/controller/health.controller.ts b/libs/health/src/controller/health.controller.ts index e29e304..0551121 100644 --- a/libs/health/src/controller/health.controller.ts +++ b/libs/health/src/controller/health.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, HttpStatus, Inject, Logger } from '@nestjs/common'; +import { Controller, Get, HttpStatus, Inject } from '@nestjs/common'; import { SkipThrottle } from '@nestjs/throttler'; import { HealthService } from '../health.service'; import { GetHealthSwagger, GetPingSwagger } from './health.swagger'; @@ -9,8 +9,6 @@ import { BaseException } from '@shared/error'; @Controller() @ApiTags('System') export class HealthController { - private logger = new Logger(HealthController.name); - constructor( private readonly healthService: HealthService, @Inject('SERVICE_NAME') private readonly serviceName: string, @@ -22,7 +20,6 @@ export class HealthController { const pingData = await this.healthService.getHealthData(); if (pingData.status !== 'up') { - this.logger.error(`${this.serviceName} is unhealthy!`); throw new BaseException( { code: 'SERVICE_UNHEALTHY', diff --git a/src/shared/error/filter.ts b/src/shared/error/filter.ts index efd4c11..202de75 100644 --- a/src/shared/error/filter.ts +++ b/src/shared/error/filter.ts @@ -156,6 +156,8 @@ export class GlobalExceptionFilter implements ExceptionFilter { const { request, response } = this.getCtxBase(host); const status = HttpStatus.INTERNAL_SERVER_ERROR; + this.log(exception, host, status, { type: 'UNKNOWN_SERVER_ERROR' }); + return response.status(status).send( this.formatErrorResponse(request, status, { code: 'INTERNAL_SERVER_ERROR', @@ -214,24 +216,21 @@ export class GlobalExceptionFilter implements ExceptionFilter { extraData: Record = {}, ) { const { request } = this.getCtxBase(host); + const requestId = request.id ?? request.headers['x-request-id']; - const logContext = { - status, - path: request.url, - method: request.method, - requestId: request.id ?? request.headers['x-request-id'], + const logMetadata = { + requestId, ...extraData, - ...(status >= 500 && { - stack: exception instanceof Error ? exception.stack : exception, - }), + timestamp: new Date().toISOString(), }; const message = `[${status}] ${request.method} ${request.url} - ${exception?.message || 'Unknown Error'}`; if (status >= 500) { - this.logger.error(message, logContext); + const stack = exception instanceof Error ? exception.stack : undefined; + this.logger.error(message, stack, JSON.stringify(logMetadata)); } else { - this.logger.warn(message, logContext); + this.logger.warn(message, JSON.stringify(logMetadata)); } } } diff --git a/src/shared/media/workers/media.worker.ts b/src/shared/media/workers/media.worker.ts index 4088624..00ccf2e 100644 --- a/src/shared/media/workers/media.worker.ts +++ b/src/shared/media/workers/media.worker.ts @@ -4,12 +4,9 @@ import { MEDIA_JOBS, MEDIA_QUEUES, MEDIA_SPECS } from '../media.constant'; import { Job } from 'bullmq'; import { S3Service } from '@libs/s3'; import { dirname } from 'path'; -import { Logger } from '@nestjs/common'; @Processor(MEDIA_QUEUES.RESIZE) export class MediaProcessor extends WorkerHost { - private logger = new Logger(MediaProcessor.name); - constructor( private readonly imagor: ImagorService, private readonly s3: S3Service, @@ -21,7 +18,6 @@ export class MediaProcessor extends WorkerHost { if (job.name !== MEDIA_JOBS.RESIZE_IMAGES) return; const { original: originalFilePath, context } = job.data; - const jobId = job.id; try { await job.updateProgress(5); @@ -59,7 +55,6 @@ export class MediaProcessor extends WorkerHost { }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`[Job:${jobId}] Resize failed: ${errorMessage}`); await job.log(`Error during resizing: ${errorMessage}`); throw error; diff --git a/src/teams/infrastructure/listeners/update-media.listener.ts b/src/teams/infrastructure/listeners/update-media.listener.ts index 28db45d..3a9f2fd 100644 --- a/src/teams/infrastructure/listeners/update-media.listener.ts +++ b/src/teams/infrastructure/listeners/update-media.listener.ts @@ -1,15 +1,13 @@ -import { Inject, Logger } from '@nestjs/common'; +import { Inject } from '@nestjs/common'; import { ITeamsRepository } from '@core/teams/domain/repository'; import { Processor, WorkerHost } from '@nestjs/bullmq'; -import { Job, UnrecoverableError } from 'bullmq'; +import { type Job, UnrecoverableError } from 'bullmq'; import { MEDIA_JOBS, MEDIA_QUEUES, type UpdateMediaTeam } from '@shared/media'; import { TeamMemberPolicy } from '@core/teams/domain/policy'; import type { TeamRole } from '@shared/entities'; @Processor(MEDIA_QUEUES.SAVE_ENTITY) export class UpdateTeamMediaListener extends WorkerHost { - private readonly logger = new Logger(UpdateTeamMediaListener.name); - constructor( @Inject('ITeamsRepository') private readonly repository: ITeamsRepository, @@ -28,9 +26,9 @@ export class UpdateTeamMediaListener extends WorkerHost { await this.executeMediaUpdate(teamId, type, path); - this.logger.log(`Successfully updated ${type} for team ${entity.slug}`); + await job.log(`Successfully updated ${type} for team ${entity.slug}`); } catch (error) { - this.logger.error( + await job.log( `Failed to update ${type} for team ${entity.slug}: ${error instanceof Error ? error.message : String(error)}`, ); throw error; diff --git a/src/user/infrastructure/listeners/update-avatar.listener.ts b/src/user/infrastructure/listeners/update-avatar.listener.ts index c386443..7563c22 100644 --- a/src/user/infrastructure/listeners/update-avatar.listener.ts +++ b/src/user/infrastructure/listeners/update-avatar.listener.ts @@ -1,13 +1,11 @@ import { IUserRepository } from '@core/user/domain/repository'; import { Processor, WorkerHost } from '@nestjs/bullmq'; -import { Inject, Logger } from '@nestjs/common'; +import { Inject } from '@nestjs/common'; import { MEDIA_JOBS, MEDIA_QUEUES, type UpdateMediaUser } from '@shared/media'; import { UnrecoverableError, type Job } from 'bullmq'; @Processor(MEDIA_QUEUES.SAVE_ENTITY) export class UpdateAvatarListener extends WorkerHost { - private readonly logger = new Logger(UpdateAvatarListener.name); - constructor( @Inject('IUserRepository') private readonly repository: IUserRepository, @@ -19,7 +17,6 @@ export class UpdateAvatarListener extends WorkerHost { if (job.name !== MEDIA_JOBS.UPDATE_USER_AVATAR) return; const { entity, path } = job.data; - const jobId = job.id; try { await job.updateProgress(10); @@ -35,7 +32,6 @@ export class UpdateAvatarListener extends WorkerHost { const userAccount = await this.repository.findById(entity.id); if (!userAccount) { - this.logger.warn(`[Job:${jobId}] User ${entity.id} not found. Skipping update.`); await job.log(`User ${entity.id} missing in database.`); return { status: 'aborted', reason: 'USER_NOT_FOUND' }; } @@ -46,12 +42,10 @@ export class UpdateAvatarListener extends WorkerHost { await job.updateProgress(100); - this.logger.log( - `[Job:${jobId}] Successfully updated avatar for user ${userAccount.user.id}`, - ); + await job.log(`Successfully updated avatar for user ${userAccount.user.id}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error(`[Job:${jobId}] Critical failure: ${errorMessage}`); + await job.log(`Critical failure: ${errorMessage}`); throw error; } From cfb79d0242bf775675d72cd95cb4f9076f15b8de Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 8 May 2026 17:01:50 +0300 Subject: [PATCH 3/6] ci: errors build --- Dockerfile.prod | 8 ++++---- package.json | 1 - pnpm-lock.yaml | 35 ----------------------------------- 3 files changed, 4 insertions(+), 40 deletions(-) diff --git a/Dockerfile.prod b/Dockerfile.prod index e645b06..e6a6c37 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -1,4 +1,4 @@ -FROM node:20-alpine AS base +FROM node:22-alpine AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" @@ -9,7 +9,7 @@ WORKDIR /app FROM base AS fetch -COPY pnpm-lock.yaml ./ +COPY pnpm-lock.yaml package.json pnpm-workspace.yaml ./ # Загружаем всё в виртуальное хранилище. # Если lock-файл не менялся, этот слой будет взят из кэша @@ -21,7 +21,7 @@ FROM fetch AS build COPY package.json ./ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ - pnpm install --frozen-lockfile --offline + pnpm install -w --frozen-lockfile --offline COPY . . @@ -30,7 +30,7 @@ RUN pnpm run build RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ pnpm prune --prod --ignore-scripts -FROM node:20-alpine AS runner +FROM node:22-alpine AS runner WORKDIR /app diff --git a/package.json b/package.json index ea112c6..c7d6c1d 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ "transliteration": "^2.6.1", "ua-parser-js": "^2.0.9", "winston": "^3.19.0", - "winston-daily-rotate-file": "^5.0.0", "zod": "^4.3.6" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b07dd6..94c94b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,9 +149,6 @@ importers: winston: specifier: ^3.19.0 version: 3.19.0 - winston-daily-rotate-file: - specifier: ^5.0.0 - version: 5.0.0(winston@3.19.0) zod: specifier: ^4.3.6 version: 4.3.6 @@ -3054,9 +3051,6 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} - file-stream-rotator@0.6.1: - resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} - file-type@21.3.4: resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} engines: {node: '>=20'} @@ -3658,9 +3652,6 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} - moment@2.30.1: - resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3728,10 +3719,6 @@ packages: resolution: {integrity: sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==} engines: {node: '>=6.0.0'} - object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} - obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -4514,12 +4501,6 @@ packages: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} - winston-daily-rotate-file@5.0.0: - resolution: {integrity: sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==} - engines: {node: '>=8'} - peerDependencies: - winston: ^3 - winston-transport@4.9.0: resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} engines: {node: '>= 12.0.0'} @@ -7677,10 +7658,6 @@ snapshots: dependencies: flat-cache: 3.2.0 - file-stream-rotator@0.6.1: - dependencies: - moment: 2.30.1 - file-type@21.3.4: dependencies: '@tokenizer/inflate': 0.4.1 @@ -8258,8 +8235,6 @@ snapshots: minipass@7.1.3: {} - moment@2.30.1: {} - ms@2.1.3: {} msgpackr-extract@3.0.3: @@ -8320,8 +8295,6 @@ snapshots: nodemailer@8.0.5: {} - object-hash@3.0.0: {} - obug@2.1.1: {} on-exit-leak-free@2.1.2: {} @@ -9049,14 +9022,6 @@ snapshots: string-width: 4.2.3 optional: true - winston-daily-rotate-file@5.0.0(winston@3.19.0): - dependencies: - file-stream-rotator: 0.6.1 - object-hash: 3.0.0 - triple-beam: 1.4.1 - winston: 3.19.0 - winston-transport: 4.9.0 - winston-transport@4.9.0: dependencies: logform: 2.7.0 From f5abaabb876621f35c3b2382b98326ae816a6b7e Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 8 May 2026 17:29:12 +0300 Subject: [PATCH 4/6] feat(logger): add Loki transport for Winston --- .env.example | 4 +- libs/bootstrap/src/bootstrap.ts | 8 +- libs/bootstrap/src/setups/index.ts | 2 +- libs/bootstrap/src/setups/logger.ts | 139 ++++++++---- libs/config/src/config.schema.ts | 1 + package.json | 1 + pnpm-lock.yaml | 316 ++++++++++++++++++++++++++++ tsconfig.json | 1 + 8 files changed, 418 insertions(+), 54 deletions(-) diff --git a/.env.example b/.env.example index f98700d..8f2001d 100644 --- a/.env.example +++ b/.env.example @@ -49,4 +49,6 @@ S3_ACCESS_KEY='' S3_SECRET_KEY='' IMAGOR_SECRET='' -IMAGOR_URL='' \ No newline at end of file +IMAGOR_URL='' + +LOKI_HOST='' \ No newline at end of file diff --git a/libs/bootstrap/src/bootstrap.ts b/libs/bootstrap/src/bootstrap.ts index 25d56d6..50a3818 100644 --- a/libs/bootstrap/src/bootstrap.ts +++ b/libs/bootstrap/src/bootstrap.ts @@ -2,7 +2,7 @@ import { Logger, VersioningType } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; import { DEFAULT_THROTTLER_OPTIONS } from './configs/throttler'; -import { setupCors, setupLogger, setupThrottler, setupSwagger, LoggingInterceptor } from './setups'; +import { setupCors, setupLogger, setupThrottler, setupSwagger } from './setups'; import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; import type { BootstrapOptions } from './interfaces/options.interface'; import fastifyCookie from '@fastify/cookie'; @@ -21,8 +21,6 @@ export async function bootstrapApp(options: BootstrapOptions) { }, }); - const winston = setupLogger(options.serviceName); - const { appModule, apiPrefix, @@ -48,8 +46,6 @@ export async function bootstrapApp(options: BootstrapOptions) { bufferLogs: true, }); - app.useLogger(winston); - const logger = new Logger(serviceName[0].toUpperCase() + serviceName.slice(1)); const configService = app.get(ConfigService); const port = configService.getOrThrow(portEnvKey, defaultPort); @@ -64,7 +60,7 @@ export async function bootstrapApp(options: BootstrapOptions) { return payload; }); - app.useGlobalInterceptors(new LoggingInterceptor()); + await setupLogger(app, options.serviceName); await app.register(fastifyCompress, { global: true, diff --git a/libs/bootstrap/src/setups/index.ts b/libs/bootstrap/src/setups/index.ts index e066846..e5d3f07 100644 --- a/libs/bootstrap/src/setups/index.ts +++ b/libs/bootstrap/src/setups/index.ts @@ -1,4 +1,4 @@ export { setupCors } from './cors'; export { setupThrottler } from './throttler'; export { setupSwagger } from './swagger'; -export { setupLogger, LoggingInterceptor } from './logger'; +export { setupLogger } from './logger'; diff --git a/libs/bootstrap/src/setups/logger.ts b/libs/bootstrap/src/setups/logger.ts index 2ce6b19..af6de75 100644 --- a/libs/bootstrap/src/setups/logger.ts +++ b/libs/bootstrap/src/setups/logger.ts @@ -9,42 +9,54 @@ import { Observable, throwError } from 'rxjs'; import { tap, catchError } from 'rxjs/operators'; import type { FastifyRequest } from 'fastify'; import { WinstonModule, utilities } from 'nest-winston'; -import { format, transports } from 'winston'; +import { format, transport, transports } from 'winston'; +import Loki from 'winston-loki'; +import type { NestFastifyApplication } from '@nestjs/platform-fastify'; +import { ConfigService } from '@nestjs/config'; -export function setupLogger(service: string) { - const isProduction = process.env.NODE_ENV === 'production'; +export function setupLogger(app: NestFastifyApplication, service: string) { + const cfg = app.get(ConfigService); - return WinstonModule.createLogger({ - level: isProduction ? 'info' : 'debug', - transports: [ - new transports.Console({ - format: format.combine( - format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), - format.ms(), - format.errors({ stack: true }), - format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'context'] }), - format((info) => { - const mask = (obj: any) => { - const sensitive = ['password', 'token', 'secret', 'authorization']; - for (const key in obj) { - if (sensitive.includes(key.toLowerCase())) obj[key] = '***'; - else if (typeof obj[key] === 'object') mask(obj[key]); - } - }; - if (info.metadata) mask(info.metadata); - return info; - })(), - - isProduction - ? format.json() - : utilities.format.nestLike(service, { - colors: true, - prettyPrint: false, - }), - ), + const isProduction = cfg.get('NODE_ENV') === 'production'; + const loki = cfg.get('LOKI_HOST'); + + const transportsList: transport[] = [ + new transports.Console({ + format: format.combine( + format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + format.ms(), + format.errors({ stack: true }), + + isProduction + ? format.json() + : utilities.format.nestLike(service, { + colors: true, + prettyPrint: false, + }), + ), + }), + ]; + + if (loki) { + transportsList.push( + new Loki({ + host: loki, + labels: { app: service }, + json: true, + format: format.json(), + replaceTimestamp: true, + onConnectionError: (err) => console.error('Loki connection error:', err), }), - ], + ); + } + + const logger = WinstonModule.createLogger({ + level: isProduction ? 'info' : 'debug', + transports: transportsList, }); + + app.useLogger(logger); + app.useGlobalInterceptors(new LoggingInterceptor()); } @Injectable() @@ -60,31 +72,66 @@ export class LoggingInterceptor implements NestInterceptor { const userAgent = headers['user-agent'] || 'unknown'; const startTime = Date.now(); + const controllerName = context.getClass().name; + const handlerName = context.getHandler().name; + const sanitizedBody = this.sanitize(body); - const queryPart = Object.keys(query).length ? `| Query: ${JSON.stringify(query)} ` : ''; - const bodyPart = - sanitizedBody && Object.keys(sanitizedBody).length - ? `| Body: ${JSON.stringify(sanitizedBody)} ` - : ''; - - this.logger.log( - `[${method}][${requestId}] ${url} ${queryPart}${bodyPart}| IP: ${ip} | UA: ${userAgent}`, - ); + const referer = headers['referer'] || 'direct'; + + this.logger.log(`--> ${method} ${url}`, { + context: 'HTTP', + type: 'request_incoming', + method, + url, + path: url.split('?')[0], + requestId, + controller: controllerName, + handler: handlerName, + ip, + userAgent, + referer, + protocol: request.protocol, + body: sanitizedBody, + query: query, + }); return next.handle().pipe( tap(() => { const delay = Date.now() - startTime; - this.logger.log(`[${method}][${requestId}] ${url} | Success | ${delay}ms`); + this.logger.log(`<-- ${method} ${url} | 200 | ${delay}ms`, { + context: 'HTTP', + type: 'request_completed', + method, + url, + requestId, + controller: controllerName, + handler: handlerName, + ip, + delay_num: delay, + status: 'success', + statusCode: 200, + }); }), catchError((err) => { const delay = Date.now() - startTime; const statusCode = err.status || err.statusCode || 500; - this.logger.error( - `[${method}][${requestId}] ${url} | Status: ${statusCode} | ${delay}ms | Msg: ${err.message}`, - err.stack, - ); + this.logger.error(`<-- ${method} ${url} | ${statusCode} | ${delay}ms`, { + context: 'HTTP', + type: 'request_error', + method, + url, + requestId, + controller: controllerName, + handler: handlerName, + ip, + statusCode, + delay_num: delay, + status: 'error', + errorMessage: err?.message, + errorStack: err?.stack, + }); return throwError(() => err); }), diff --git a/libs/config/src/config.schema.ts b/libs/config/src/config.schema.ts index 0a6fd41..44a9e15 100644 --- a/libs/config/src/config.schema.ts +++ b/libs/config/src/config.schema.ts @@ -91,6 +91,7 @@ export const ConfigSchema = z.object({ S3_SECRET_KEY: z.string({ error: 'S3_SECRET_KEY is missing (MinIO root password or IAM secret)', }), + LOKI_HOST: z.string().url().min(1).optional(), }); export type Config = z.infer; diff --git a/package.json b/package.json index c7d6c1d..45f2af3 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "transliteration": "^2.6.1", "ua-parser-js": "^2.0.9", "winston": "^3.19.0", + "winston-loki": "^6.1.4", "zod": "^4.3.6" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94c94b4..9d960e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,6 +149,9 @@ importers: winston: specifier: ^3.19.0 version: 3.19.0 + winston-loki: + specifier: ^6.1.4 + version: 6.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) zod: specifier: ^4.3.6 version: 4.3.6 @@ -1321,6 +1324,120 @@ packages: cpu: [x64] os: [win32] + '@napi-rs/snappy-android-arm-eabi@7.3.3': + resolution: {integrity: sha512-d4vUFFzNBvazGfB/KU8MnEax6itTIgRWXodPdZDnWKHy9HwVBndpCiedQDcSNHcZNYV36rx034rpn7SAuTL2NA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@napi-rs/snappy-android-arm64@7.3.3': + resolution: {integrity: sha512-Uh+w18dhzjVl85MGhRnojb7OLlX2ErvMsYIunO/7l3Frvc2zQvfqsWsFJanu2dwqlE2YDooeNP84S+ywgN9sxg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/snappy-darwin-arm64@7.3.3': + resolution: {integrity: sha512-AmJn+6yOu/0V0YNHLKmRUNYkn93iv/1wtPayC7O1OHtfY6YqHQ31/MVeeRBiEYtQW9TwVZxXrDirxSB1PxRdtw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/snappy-darwin-x64@7.3.3': + resolution: {integrity: sha512-biLTXBmPjPmO7HIpv+5BaV9Gy/4+QJSUNJW8Pjx1UlWAVnocPy7um+zbvAWStZssTI5sfn/jOClrAegD4w09UA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/snappy-freebsd-x64@7.3.3': + resolution: {integrity: sha512-E3R3ewm8Mrjm0yL2TC3VgnphDsQaCPixNJqBbGiz3NTshVDhlPlOgPKF0NGYqKiKaDGdD9PKtUgOR4vagUtn7g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@napi-rs/snappy-linux-arm-gnueabihf@7.3.3': + resolution: {integrity: sha512-ZuNgtmk9j0KyT7TfLyEnvZJxOhbkyNR761nk04F0Q4NTHMICP28wQj0xgEsnCHUsEeA9OXrRL4R7waiLn+rOQA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/snappy-linux-arm64-gnu@7.3.3': + resolution: {integrity: sha512-KIzwtq0dAzshzpqZWjg0Q9lUx93iZN7wCCUzCdLYIQ+mvJZKM10VCdn0RcuQze1R3UJTPwpPLXQIVskNMBYyPA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/snappy-linux-arm64-musl@7.3.3': + resolution: {integrity: sha512-AAED4cQS74xPvktsyVmz5sy8vSxG/+3d7Rq2FDBZzj3Fv6v5vux6uZnECPCAqpALCdTtJ61unqpOyqO7hZCt1Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/snappy-linux-ppc64-gnu@7.3.3': + resolution: {integrity: sha512-pofO5eSLg8ZTBwVae4WHHwJxJGZI8NEb4r5Mppvq12J/1/Hq1HecClXmfY3A7bdT2fsS2Td+Q7CI9VdBOj2sbA==} + engines: {node: '>= 10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@napi-rs/snappy-linux-riscv64-gnu@7.3.3': + resolution: {integrity: sha512-OiHYdeuwj0TVBXADUmmQDQ4lL1TB+8EwmXnFgOutoDVXHaUl0CJFyXLa6tYUXe+gRY8hs1v7eb0vyE97LKY06Q==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@napi-rs/snappy-linux-s390x-gnu@7.3.3': + resolution: {integrity: sha512-66QdmuV9CTq/S/xifZXlMy3PsZTviAgkqqpZ+7vPCmLtuP+nqhaeupShOFf/sIDsS0gZePazPosPTeTBbhkLHg==} + engines: {node: '>= 10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@napi-rs/snappy-linux-x64-gnu@7.3.3': + resolution: {integrity: sha512-g6KURjOxrgb8yXDEZMuIcHkUr/7TKlDwSiydEQtMtP3n4iI4sNjkcE/WNKlR3+t9bZh1pFGAq7NFRBtouQGHpQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/snappy-linux-x64-musl@7.3.3': + resolution: {integrity: sha512-6UvOyczHknpaKjrlKKSlX3rwpOrfJwiMG6qA0NRKJFgbcCAEUxmN9A8JvW4inP46DKdQ0bekdOxwRtAhFiTDfg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/snappy-openharmony-arm64@7.3.3': + resolution: {integrity: sha512-I5mak/5rTprobf7wMCk0vFhClmWOL/QiIJM4XontysnadmP/R9hAcmuFmoMV2GaxC9MblqLA7Z++gy8ou5hJVw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [openharmony] + + '@napi-rs/snappy-wasm32-wasi@7.3.3': + resolution: {integrity: sha512-+EroeygVYo9RksOchjF206frhMkfD2PaIun3yH4Zp5j/Y0oIEgs/+VhAYx/f+zHRylQYUIdLzDRclcoepvlR8Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@napi-rs/snappy-win32-arm64-msvc@7.3.3': + resolution: {integrity: sha512-rxqfntBsCfzgOha/OlG8ld2hs6YSMGhpMUbFjeQLyVDbooY041fRXv3S7yk52DfO6H4QQhLT5+p7cW0mYdhyiQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/snappy-win32-ia32-msvc@7.3.3': + resolution: {integrity: sha512-joRV16DsRtqjGt0CdSpxGCkO0UlHGeTZ/GqvdscoALpRKbikR2Top4C61dxEchmOd3lSYsXutuwWWGg3Nr++WA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@napi-rs/snappy-win32-x64-msvc@7.3.3': + resolution: {integrity: sha512-cEnQwcsdJyOU7HSZODWsHpKuQoSYM4jaqw/hn9pOXYbRN1+02WxYppD3fdMuKN6TOA6YG5KA5PHRNeVilNX86Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@napi-rs/wasm-runtime@1.1.3': resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} peerDependencies: @@ -1594,6 +1711,36 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.1': + resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + '@rolldown/binding-android-arm64@1.0.0-rc.15': resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2371,6 +2518,10 @@ packages: ast-v8-to-istanbul@1.0.0: resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -2437,6 +2588,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + btoa@1.2.1: + resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==} + engines: {node: '>= 0.4.0'} + hasBin: true + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -3565,6 +3721,9 @@ packages: resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@11.3.3: resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==} engines: {node: 20 || >=22} @@ -3919,6 +4078,10 @@ packages: resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} engines: {node: ^16 || ^18 || >=20} + protobufjs@7.5.6: + resolution: {integrity: sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==} + engines: {node: '>=12.0.0'} + proxy-from-env@2.1.0: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} @@ -4098,6 +4261,10 @@ packages: resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} engines: {node: '>=20'} + snappy@7.3.3: + resolution: {integrity: sha512-UDJVCunvgblRpfTOjo/uT7pQzfrTsSICJ4yVS4aq7SsGBaUSpJwaVP15nF//jqinSLpN7boe/BqbUmtWMTQ5MQ==} + engines: {node: '>= 10'} + sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} @@ -4362,6 +4529,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-polyfill@1.1.14: + resolution: {integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -4501,6 +4671,9 @@ packages: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} + winston-loki@6.1.4: + resolution: {integrity: sha512-/j/Zf7TGLjgSBck29BkPnpJlEnGQr5xqlx8A0N6LZnXYYYvyK7lFk6FPpWiD+VMO8xjaxOu1KNF9SzWaRDsigA==} + winston-transport@4.9.0: resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} engines: {node: '>= 12.0.0'} @@ -5828,6 +6001,65 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true + '@napi-rs/snappy-android-arm-eabi@7.3.3': + optional: true + + '@napi-rs/snappy-android-arm64@7.3.3': + optional: true + + '@napi-rs/snappy-darwin-arm64@7.3.3': + optional: true + + '@napi-rs/snappy-darwin-x64@7.3.3': + optional: true + + '@napi-rs/snappy-freebsd-x64@7.3.3': + optional: true + + '@napi-rs/snappy-linux-arm-gnueabihf@7.3.3': + optional: true + + '@napi-rs/snappy-linux-arm64-gnu@7.3.3': + optional: true + + '@napi-rs/snappy-linux-arm64-musl@7.3.3': + optional: true + + '@napi-rs/snappy-linux-ppc64-gnu@7.3.3': + optional: true + + '@napi-rs/snappy-linux-riscv64-gnu@7.3.3': + optional: true + + '@napi-rs/snappy-linux-s390x-gnu@7.3.3': + optional: true + + '@napi-rs/snappy-linux-x64-gnu@7.3.3': + optional: true + + '@napi-rs/snappy-linux-x64-musl@7.3.3': + optional: true + + '@napi-rs/snappy-openharmony-arm64@7.3.3': + optional: true + + '@napi-rs/snappy-wasm32-wasi@7.3.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@napi-rs/snappy-win32-arm64-msvc@7.3.3': + optional: true + + '@napi-rs/snappy-win32-ia32-msvc@7.3.3': + optional: true + + '@napi-rs/snappy-win32-x64-msvc@7.3.3': + optional: true + '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: '@emnapi/core': 1.9.2 @@ -6087,6 +6319,29 @@ snapshots: '@pinojs/redact@0.4.0': {} + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.1 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.1': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.1': {} + '@rolldown/binding-android-arm64@1.0.0-rc.15': optional: true @@ -6985,6 +7240,8 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 + async-exit-hook@2.0.1: {} + async@3.2.6: {} asynckit@0.4.0: {} @@ -7061,6 +7318,8 @@ snapshots: node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) + btoa@1.2.1: {} + buffer-equal-constant-time@1.0.1: {} buffer-from@1.1.2: {} @@ -8164,6 +8423,8 @@ snapshots: safe-stable-stringify: 2.5.0 triple-beam: 1.4.1 + long@5.3.2: {} + lru-cache@11.3.3: {} luxon@3.7.2: {} @@ -8500,6 +8761,21 @@ snapshots: '@opentelemetry/api': 1.9.1 tdigest: 0.1.2 + protobufjs@7.5.6: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.1 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/node': 20.19.39 + long: 5.3.2 + proxy-from-env@2.1.0: {} pump@3.0.4: @@ -8679,6 +8955,31 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + snappy@7.3.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + optionalDependencies: + '@napi-rs/snappy-android-arm-eabi': 7.3.3 + '@napi-rs/snappy-android-arm64': 7.3.3 + '@napi-rs/snappy-darwin-arm64': 7.3.3 + '@napi-rs/snappy-darwin-x64': 7.3.3 + '@napi-rs/snappy-freebsd-x64': 7.3.3 + '@napi-rs/snappy-linux-arm-gnueabihf': 7.3.3 + '@napi-rs/snappy-linux-arm64-gnu': 7.3.3 + '@napi-rs/snappy-linux-arm64-musl': 7.3.3 + '@napi-rs/snappy-linux-ppc64-gnu': 7.3.3 + '@napi-rs/snappy-linux-riscv64-gnu': 7.3.3 + '@napi-rs/snappy-linux-s390x-gnu': 7.3.3 + '@napi-rs/snappy-linux-x64-gnu': 7.3.3 + '@napi-rs/snappy-linux-x64-musl': 7.3.3 + '@napi-rs/snappy-openharmony-arm64': 7.3.3 + '@napi-rs/snappy-wasm32-wasi': 7.3.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@napi-rs/snappy-win32-arm64-msvc': 7.3.3 + '@napi-rs/snappy-win32-ia32-msvc': 7.3.3 + '@napi-rs/snappy-win32-x64-msvc': 7.3.3 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 @@ -8910,6 +9211,8 @@ snapshots: dependencies: punycode: 2.3.1 + url-polyfill@1.1.14: {} + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} @@ -9022,6 +9325,19 @@ snapshots: string-width: 4.2.3 optional: true + winston-loki@6.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + dependencies: + async-exit-hook: 2.0.1 + btoa: 1.2.1 + protobufjs: 7.5.6 + url-polyfill: 1.1.14 + winston-transport: 4.9.0 + optionalDependencies: + snappy: 7.3.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + winston-transport@4.9.0: dependencies: logform: 2.7.0 diff --git a/tsconfig.json b/tsconfig.json index 12de8aa..c260ad5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "target": "ES2021", + "esModuleInterop": true, "sourceMap": true, "outDir": "./dist", "incremental": true, From 440d010ea34206df974a314ffb357696eee38350 Mon Sep 17 00:00:00 2001 From: soorq Date: Sat, 9 May 2026 14:30:12 +0300 Subject: [PATCH 5/6] feat(logging): refactor to structured JSON logging and unified log schema --- .env.example | 2 - libs/bootstrap/src/setups/logger.ts | 255 +++++++++++++--------- libs/config/src/config.schema.ts | 1 - package.json | 1 - pnpm-lock.yaml | 316 ---------------------------- src/shared/error/filter.ts | 26 ++- 6 files changed, 176 insertions(+), 425 deletions(-) diff --git a/.env.example b/.env.example index 8f2001d..44b4e59 100644 --- a/.env.example +++ b/.env.example @@ -50,5 +50,3 @@ S3_SECRET_KEY='' IMAGOR_SECRET='' IMAGOR_URL='' - -LOKI_HOST='' \ No newline at end of file diff --git a/libs/bootstrap/src/setups/logger.ts b/libs/bootstrap/src/setups/logger.ts index af6de75..f7e6810 100644 --- a/libs/bootstrap/src/setups/logger.ts +++ b/libs/bootstrap/src/setups/logger.ts @@ -9,50 +9,24 @@ import { Observable, throwError } from 'rxjs'; import { tap, catchError } from 'rxjs/operators'; import type { FastifyRequest } from 'fastify'; import { WinstonModule, utilities } from 'nest-winston'; -import { format, transport, transports } from 'winston'; -import Loki from 'winston-loki'; +import { format, transports } from 'winston'; import type { NestFastifyApplication } from '@nestjs/platform-fastify'; import { ConfigService } from '@nestjs/config'; export function setupLogger(app: NestFastifyApplication, service: string) { const cfg = app.get(ConfigService); - const isProduction = cfg.get('NODE_ENV') === 'production'; - const loki = cfg.get('LOKI_HOST'); - - const transportsList: transport[] = [ - new transports.Console({ - format: format.combine( - format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), - format.ms(), - format.errors({ stack: true }), - - isProduction - ? format.json() - : utilities.format.nestLike(service, { - colors: true, - prettyPrint: false, - }), - ), - }), - ]; - - if (loki) { - transportsList.push( - new Loki({ - host: loki, - labels: { app: service }, - json: true, - format: format.json(), - replaceTimestamp: true, - onConnectionError: (err) => console.error('Loki connection error:', err), - }), - ); - } const logger = WinstonModule.createLogger({ level: isProduction ? 'info' : 'debug', - transports: transportsList, + format: format.combine( + format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }), + format.errors({ stack: true }), + isProduction + ? format.json() + : format.combine(format.ms(), utilities.format.nestLike(service, { colors: true })), + ), + transports: [new transports.Console()], }); app.useLogger(logger); @@ -66,72 +40,54 @@ export class LoggingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); - const { method, url, body, query, ip, headers } = request; - - const requestId = request.id || request.headers['x-request-id'] || 'unknown'; - const userAgent = headers['user-agent'] || 'unknown'; const startTime = Date.now(); - const controllerName = context.getClass().name; - const handlerName = context.getHandler().name; - - const sanitizedBody = this.sanitize(body); - const referer = headers['referer'] || 'direct'; - - this.logger.log(`--> ${method} ${url}`, { - context: 'HTTP', - type: 'request_incoming', - method, - url, - path: url.split('?')[0], - requestId, - controller: controllerName, - handler: handlerName, - ip, - userAgent, - referer, - protocol: request.protocol, - body: sanitizedBody, - query: query, + const baseCtx = { + request_id: request.id || request.headers['x-request-id'] || 'unknown', + method: request.method, + url: request.url, + path: request.url.split('?')[0], + controller: context.getClass().name, + handler: context.getHandler().name, + ip: request.ip, + referer: request.headers['referer'] || 'direct', + user_agent: request.headers['user-agent'] || 'unknown', + triggered_by: 'interceptor', + }; + + this.logger.log(`Incoming ${baseCtx.method} ${baseCtx.url}`, { + ...baseCtx, + type: 'request', + body: this.sanitize(request.body), + query: request.query, }); return next.handle().pipe( tap(() => { - const delay = Date.now() - startTime; - - this.logger.log(`<-- ${method} ${url} | 200 | ${delay}ms`, { - context: 'HTTP', - type: 'request_completed', - method, - url, - requestId, - controller: controllerName, - handler: handlerName, - ip, - delay_num: delay, - status: 'success', - statusCode: 200, + const delay_num = Date.now() - startTime; + + this.logger.log(`${baseCtx.method} ${baseCtx.path} | 200 | ${delay_num}ms`, { + ...baseCtx, + type: 'response', + status_code: 200, + delay_num, }); }), catchError((err) => { - const delay = Date.now() - startTime; - const statusCode = err.status || err.statusCode || 500; - - this.logger.error(`<-- ${method} ${url} | ${statusCode} | ${delay}ms`, { - context: 'HTTP', - type: 'request_error', - method, - url, - requestId, - controller: controllerName, - handler: handlerName, - ip, - statusCode, - delay_num: delay, - status: 'error', - errorMessage: err?.message, - errorStack: err?.stack, - }); + const delay_num = Date.now() - startTime; + const status_code = err.status || err.statusCode || 500; + + this.logger.error( + `${baseCtx.method} ${baseCtx.path} | ${status_code} | ${delay_num}ms`, + { + ...baseCtx, + type: 'error', + status_code, + delay_num, + stack: err.stack, + error_details: err.response || err.message, + }, + ); return throwError(() => err); }), @@ -142,19 +98,126 @@ export class LoggingInterceptor implements NestInterceptor { if (!data || typeof data !== 'object') return data; if (Array.isArray(data)) return data.map((v) => this.sanitize(v)); - return Object.keys(data).reduce((acc, key) => { + const cleanData = JSON.parse(JSON.stringify(data)); + + return Object.keys(cleanData).reduce((acc, key) => { const isSensitive = this.sensitiveFields.some((field) => key.toLowerCase().includes(field), ); if (isSensitive) { acc[key] = '***'; - } else if (typeof data[key] === 'object') { - acc[key] = this.sanitize(data[key]); + } else if (typeof cleanData[key] === 'object') { + acc[key] = this.sanitize(cleanData[key]); } else { - acc[key] = data[key]; + acc[key] = cleanData[key]; } return acc; }, {}); } } + +/** + * Represents a structured application log payload for Grafana Loki. + * This object is flattened to ensure each property is indexed as a top-level label/column. + * + * @typedef {Object} TLog + */ +export type TLog = { + /** + * The severity level of the log. + * Used by Grafana to color-code rows and for alerting. + * @type {'info' | 'error' | 'warn'} + */ + level: 'info' | 'error' | 'warn'; + /** + * Human-readable summary of the event. + * @example 'Request completed POST /v1/auth/sign-in | 200 | 145ms' + * @type {string} + */ + message: string; + /** + * Event occurrence time in ISO 8601 format. + * @example '2026-05-09T01:17:29.000Z' + * @type {string} + */ + timestamp: string; + /** + * Unique identifier for the HTTP request (e.g., UUID, NanoID). + * Used to correlate all logs produced within a single request lifecycle. + * @type {string} + */ + request_id: string; + /** + * The system component that triggered the log entry. + * @type {'interceptor' | 'filter_exception' | 'guard' | 'service'} + */ + triggered_by: 'interceptor' | 'filter_exception' | 'guard' | 'service'; + /** + * The logical type of the event within the request/response flow. + * @type {'request' | 'response' | 'error' | 'system'} + */ + type: 'request' | 'response' | 'error' | 'system'; + /** + * The HTTP method used for the request. + * @type {'POST' | 'GET' | 'DELETE' | 'PATCH' | 'PUT' | 'OPTIONS' | 'HEAD'} + */ + method: 'POST' | 'GET' | 'DELETE' | 'PATCH' | 'PUT' | 'OPTIONS' | 'HEAD'; + /** + * The full URL of the request, including query parameters. + * @example '/v1/auth/sign-in?source=mobile' + * @type {string} + */ + url: string; + /** + * The sanitized API path, including versioning but excluding query parameters. + * Ideal for aggregating statistics per endpoint. + * @example '/v1/auth/sign-in' + * @type {string} + */ + path: string; + /** + * The HTTP status code returned to the client. + * @example 200 + * @type {number} + */ + status_code: number; + /** + * Request processing time in milliseconds. + * Note: Typically undefined for entries with type 'request'. + * @type {number} + */ + delay_num?: number; + /** + * The client's IP address. + * @type {string} + */ + ip: string; + /** + * The client's application or browser identification string. + * @type {string} + */ + user_agent: string; + /** + * The name of the NestJS controller handling the request. + * @example 'AuthController' + * @type {string} + */ + controller: string; + /** + * The name of the specific controller method (handler). + * @example 'signIn' + * @type {string} + */ + handler: string; + /** + * The error stack trace. Only populated when level is 'error'. + * @type {string} + */ + stack?: string; + /** + * Additional contextual data for debugging (e.g., Zod validation issues, DB error details). + * @type {any} + */ + error_details?: any; +}; diff --git a/libs/config/src/config.schema.ts b/libs/config/src/config.schema.ts index 44a9e15..0a6fd41 100644 --- a/libs/config/src/config.schema.ts +++ b/libs/config/src/config.schema.ts @@ -91,7 +91,6 @@ export const ConfigSchema = z.object({ S3_SECRET_KEY: z.string({ error: 'S3_SECRET_KEY is missing (MinIO root password or IAM secret)', }), - LOKI_HOST: z.string().url().min(1).optional(), }); export type Config = z.infer; diff --git a/package.json b/package.json index 45f2af3..c7d6c1d 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ "transliteration": "^2.6.1", "ua-parser-js": "^2.0.9", "winston": "^3.19.0", - "winston-loki": "^6.1.4", "zod": "^4.3.6" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d960e7..94c94b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,9 +149,6 @@ importers: winston: specifier: ^3.19.0 version: 3.19.0 - winston-loki: - specifier: ^6.1.4 - version: 6.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) zod: specifier: ^4.3.6 version: 4.3.6 @@ -1324,120 +1321,6 @@ packages: cpu: [x64] os: [win32] - '@napi-rs/snappy-android-arm-eabi@7.3.3': - resolution: {integrity: sha512-d4vUFFzNBvazGfB/KU8MnEax6itTIgRWXodPdZDnWKHy9HwVBndpCiedQDcSNHcZNYV36rx034rpn7SAuTL2NA==} - engines: {node: '>= 10'} - cpu: [arm] - os: [android] - - '@napi-rs/snappy-android-arm64@7.3.3': - resolution: {integrity: sha512-Uh+w18dhzjVl85MGhRnojb7OLlX2ErvMsYIunO/7l3Frvc2zQvfqsWsFJanu2dwqlE2YDooeNP84S+ywgN9sxg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - - '@napi-rs/snappy-darwin-arm64@7.3.3': - resolution: {integrity: sha512-AmJn+6yOu/0V0YNHLKmRUNYkn93iv/1wtPayC7O1OHtfY6YqHQ31/MVeeRBiEYtQW9TwVZxXrDirxSB1PxRdtw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@napi-rs/snappy-darwin-x64@7.3.3': - resolution: {integrity: sha512-biLTXBmPjPmO7HIpv+5BaV9Gy/4+QJSUNJW8Pjx1UlWAVnocPy7um+zbvAWStZssTI5sfn/jOClrAegD4w09UA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@napi-rs/snappy-freebsd-x64@7.3.3': - resolution: {integrity: sha512-E3R3ewm8Mrjm0yL2TC3VgnphDsQaCPixNJqBbGiz3NTshVDhlPlOgPKF0NGYqKiKaDGdD9PKtUgOR4vagUtn7g==} - engines: {node: '>= 10'} - cpu: [x64] - os: [freebsd] - - '@napi-rs/snappy-linux-arm-gnueabihf@7.3.3': - resolution: {integrity: sha512-ZuNgtmk9j0KyT7TfLyEnvZJxOhbkyNR761nk04F0Q4NTHMICP28wQj0xgEsnCHUsEeA9OXrRL4R7waiLn+rOQA==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - - '@napi-rs/snappy-linux-arm64-gnu@7.3.3': - resolution: {integrity: sha512-KIzwtq0dAzshzpqZWjg0Q9lUx93iZN7wCCUzCdLYIQ+mvJZKM10VCdn0RcuQze1R3UJTPwpPLXQIVskNMBYyPA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@napi-rs/snappy-linux-arm64-musl@7.3.3': - resolution: {integrity: sha512-AAED4cQS74xPvktsyVmz5sy8vSxG/+3d7Rq2FDBZzj3Fv6v5vux6uZnECPCAqpALCdTtJ61unqpOyqO7hZCt1Q==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@napi-rs/snappy-linux-ppc64-gnu@7.3.3': - resolution: {integrity: sha512-pofO5eSLg8ZTBwVae4WHHwJxJGZI8NEb4r5Mppvq12J/1/Hq1HecClXmfY3A7bdT2fsS2Td+Q7CI9VdBOj2sbA==} - engines: {node: '>= 10'} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@napi-rs/snappy-linux-riscv64-gnu@7.3.3': - resolution: {integrity: sha512-OiHYdeuwj0TVBXADUmmQDQ4lL1TB+8EwmXnFgOutoDVXHaUl0CJFyXLa6tYUXe+gRY8hs1v7eb0vyE97LKY06Q==} - engines: {node: '>= 10'} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@napi-rs/snappy-linux-s390x-gnu@7.3.3': - resolution: {integrity: sha512-66QdmuV9CTq/S/xifZXlMy3PsZTviAgkqqpZ+7vPCmLtuP+nqhaeupShOFf/sIDsS0gZePazPosPTeTBbhkLHg==} - engines: {node: '>= 10'} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@napi-rs/snappy-linux-x64-gnu@7.3.3': - resolution: {integrity: sha512-g6KURjOxrgb8yXDEZMuIcHkUr/7TKlDwSiydEQtMtP3n4iI4sNjkcE/WNKlR3+t9bZh1pFGAq7NFRBtouQGHpQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@napi-rs/snappy-linux-x64-musl@7.3.3': - resolution: {integrity: sha512-6UvOyczHknpaKjrlKKSlX3rwpOrfJwiMG6qA0NRKJFgbcCAEUxmN9A8JvW4inP46DKdQ0bekdOxwRtAhFiTDfg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@napi-rs/snappy-openharmony-arm64@7.3.3': - resolution: {integrity: sha512-I5mak/5rTprobf7wMCk0vFhClmWOL/QiIJM4XontysnadmP/R9hAcmuFmoMV2GaxC9MblqLA7Z++gy8ou5hJVw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [openharmony] - - '@napi-rs/snappy-wasm32-wasi@7.3.3': - resolution: {integrity: sha512-+EroeygVYo9RksOchjF206frhMkfD2PaIun3yH4Zp5j/Y0oIEgs/+VhAYx/f+zHRylQYUIdLzDRclcoepvlR8Q==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@napi-rs/snappy-win32-arm64-msvc@7.3.3': - resolution: {integrity: sha512-rxqfntBsCfzgOha/OlG8ld2hs6YSMGhpMUbFjeQLyVDbooY041fRXv3S7yk52DfO6H4QQhLT5+p7cW0mYdhyiQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@napi-rs/snappy-win32-ia32-msvc@7.3.3': - resolution: {integrity: sha512-joRV16DsRtqjGt0CdSpxGCkO0UlHGeTZ/GqvdscoALpRKbikR2Top4C61dxEchmOd3lSYsXutuwWWGg3Nr++WA==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - - '@napi-rs/snappy-win32-x64-msvc@7.3.3': - resolution: {integrity: sha512-cEnQwcsdJyOU7HSZODWsHpKuQoSYM4jaqw/hn9pOXYbRN1+02WxYppD3fdMuKN6TOA6YG5KA5PHRNeVilNX86Q==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - '@napi-rs/wasm-runtime@1.1.3': resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} peerDependencies: @@ -1711,36 +1594,6 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.5': - resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - - '@protobufjs/eventemitter@1.1.0': - resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} - - '@protobufjs/fetch@1.1.0': - resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/inquire@1.1.1': - resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.1': - resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} - '@rolldown/binding-android-arm64@1.0.0-rc.15': resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2518,10 +2371,6 @@ packages: ast-v8-to-istanbul@1.0.0: resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} - async-exit-hook@2.0.1: - resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} - engines: {node: '>=0.12.0'} - async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -2588,11 +2437,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - btoa@1.2.1: - resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==} - engines: {node: '>= 0.4.0'} - hasBin: true - buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -3721,9 +3565,6 @@ packages: resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - lru-cache@11.3.3: resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==} engines: {node: 20 || >=22} @@ -4078,10 +3919,6 @@ packages: resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} engines: {node: ^16 || ^18 || >=20} - protobufjs@7.5.6: - resolution: {integrity: sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==} - engines: {node: '>=12.0.0'} - proxy-from-env@2.1.0: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} @@ -4261,10 +4098,6 @@ packages: resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} engines: {node: '>=20'} - snappy@7.3.3: - resolution: {integrity: sha512-UDJVCunvgblRpfTOjo/uT7pQzfrTsSICJ4yVS4aq7SsGBaUSpJwaVP15nF//jqinSLpN7boe/BqbUmtWMTQ5MQ==} - engines: {node: '>= 10'} - sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} @@ -4529,9 +4362,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - url-polyfill@1.1.14: - resolution: {integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==} - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -4671,9 +4501,6 @@ packages: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} - winston-loki@6.1.4: - resolution: {integrity: sha512-/j/Zf7TGLjgSBck29BkPnpJlEnGQr5xqlx8A0N6LZnXYYYvyK7lFk6FPpWiD+VMO8xjaxOu1KNF9SzWaRDsigA==} - winston-transport@4.9.0: resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} engines: {node: '>= 12.0.0'} @@ -6001,65 +5828,6 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true - '@napi-rs/snappy-android-arm-eabi@7.3.3': - optional: true - - '@napi-rs/snappy-android-arm64@7.3.3': - optional: true - - '@napi-rs/snappy-darwin-arm64@7.3.3': - optional: true - - '@napi-rs/snappy-darwin-x64@7.3.3': - optional: true - - '@napi-rs/snappy-freebsd-x64@7.3.3': - optional: true - - '@napi-rs/snappy-linux-arm-gnueabihf@7.3.3': - optional: true - - '@napi-rs/snappy-linux-arm64-gnu@7.3.3': - optional: true - - '@napi-rs/snappy-linux-arm64-musl@7.3.3': - optional: true - - '@napi-rs/snappy-linux-ppc64-gnu@7.3.3': - optional: true - - '@napi-rs/snappy-linux-riscv64-gnu@7.3.3': - optional: true - - '@napi-rs/snappy-linux-s390x-gnu@7.3.3': - optional: true - - '@napi-rs/snappy-linux-x64-gnu@7.3.3': - optional: true - - '@napi-rs/snappy-linux-x64-musl@7.3.3': - optional: true - - '@napi-rs/snappy-openharmony-arm64@7.3.3': - optional: true - - '@napi-rs/snappy-wasm32-wasi@7.3.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': - dependencies: - '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - optional: true - - '@napi-rs/snappy-win32-arm64-msvc@7.3.3': - optional: true - - '@napi-rs/snappy-win32-ia32-msvc@7.3.3': - optional: true - - '@napi-rs/snappy-win32-x64-msvc@7.3.3': - optional: true - '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: '@emnapi/core': 1.9.2 @@ -6319,29 +6087,6 @@ snapshots: '@pinojs/redact@0.4.0': {} - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.5': {} - - '@protobufjs/eventemitter@1.1.0': {} - - '@protobufjs/fetch@1.1.0': - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.1 - - '@protobufjs/float@1.0.2': {} - - '@protobufjs/inquire@1.1.1': {} - - '@protobufjs/path@1.1.2': {} - - '@protobufjs/pool@1.1.0': {} - - '@protobufjs/utf8@1.1.1': {} - '@rolldown/binding-android-arm64@1.0.0-rc.15': optional: true @@ -7240,8 +6985,6 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 - async-exit-hook@2.0.1: {} - async@3.2.6: {} asynckit@0.4.0: {} @@ -7318,8 +7061,6 @@ snapshots: node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) - btoa@1.2.1: {} - buffer-equal-constant-time@1.0.1: {} buffer-from@1.1.2: {} @@ -8423,8 +8164,6 @@ snapshots: safe-stable-stringify: 2.5.0 triple-beam: 1.4.1 - long@5.3.2: {} - lru-cache@11.3.3: {} luxon@3.7.2: {} @@ -8761,21 +8500,6 @@ snapshots: '@opentelemetry/api': 1.9.1 tdigest: 0.1.2 - protobufjs@7.5.6: - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.5 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 - '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.1 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.1 - '@types/node': 20.19.39 - long: 5.3.2 - proxy-from-env@2.1.0: {} pump@3.0.4: @@ -8955,31 +8679,6 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - snappy@7.3.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): - optionalDependencies: - '@napi-rs/snappy-android-arm-eabi': 7.3.3 - '@napi-rs/snappy-android-arm64': 7.3.3 - '@napi-rs/snappy-darwin-arm64': 7.3.3 - '@napi-rs/snappy-darwin-x64': 7.3.3 - '@napi-rs/snappy-freebsd-x64': 7.3.3 - '@napi-rs/snappy-linux-arm-gnueabihf': 7.3.3 - '@napi-rs/snappy-linux-arm64-gnu': 7.3.3 - '@napi-rs/snappy-linux-arm64-musl': 7.3.3 - '@napi-rs/snappy-linux-ppc64-gnu': 7.3.3 - '@napi-rs/snappy-linux-riscv64-gnu': 7.3.3 - '@napi-rs/snappy-linux-s390x-gnu': 7.3.3 - '@napi-rs/snappy-linux-x64-gnu': 7.3.3 - '@napi-rs/snappy-linux-x64-musl': 7.3.3 - '@napi-rs/snappy-openharmony-arm64': 7.3.3 - '@napi-rs/snappy-wasm32-wasi': 7.3.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) - '@napi-rs/snappy-win32-arm64-msvc': 7.3.3 - '@napi-rs/snappy-win32-ia32-msvc': 7.3.3 - '@napi-rs/snappy-win32-x64-msvc': 7.3.3 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - optional: true - sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 @@ -9211,8 +8910,6 @@ snapshots: dependencies: punycode: 2.3.1 - url-polyfill@1.1.14: {} - util-deprecate@1.0.2: {} utils-merge@1.0.1: {} @@ -9325,19 +9022,6 @@ snapshots: string-width: 4.2.3 optional: true - winston-loki@6.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): - dependencies: - async-exit-hook: 2.0.1 - btoa: 1.2.1 - protobufjs: 7.5.6 - url-polyfill: 1.1.14 - winston-transport: 4.9.0 - optionalDependencies: - snappy: 7.3.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - winston-transport@4.9.0: dependencies: logform: 2.7.0 diff --git a/src/shared/error/filter.ts b/src/shared/error/filter.ts index 202de75..1fa7f9b 100644 --- a/src/shared/error/filter.ts +++ b/src/shared/error/filter.ts @@ -216,21 +216,29 @@ export class GlobalExceptionFilter implements ExceptionFilter { extraData: Record = {}, ) { const { request } = this.getCtxBase(host); - const requestId = request.id ?? request.headers['x-request-id']; - const logMetadata = { - requestId, - ...extraData, - timestamp: new Date().toISOString(), + const logData = { + request_id: request.id || request.headers['x-request-id'] || 'unknown', + triggered_by: 'filter_exception', + type: 'error', + method: request.method ?? 'Unknown', + url: request.url, + path: request.url.split('?')[0], + status_code: status, + ip: request.ip, + user_agent: request.headers['user-agent'] || 'unknown', + controller: 'Unknown', + handler: 'Unknown', + stack: exception instanceof Error ? exception.stack : undefined, + error_details: extraData, }; - const message = `[${status}] ${request.method} ${request.url} - ${exception?.message || 'Unknown Error'}`; + const message = `Exception Filter: ${logData.method} ${logData.path} | ${status} | ${exception?.message || 'Unknown Error'}`; if (status >= 500) { - const stack = exception instanceof Error ? exception.stack : undefined; - this.logger.error(message, stack, JSON.stringify(logMetadata)); + this.logger.error(message, logData); } else { - this.logger.warn(message, JSON.stringify(logMetadata)); + this.logger.warn(message, logData); } } } From 247717506fd46644e77c6f3b3d8379abc43cba6f Mon Sep 17 00:00:00 2001 From: soorq Date: Sun, 10 May 2026 19:54:44 +0300 Subject: [PATCH 6/6] refactor: implement generic per sanitaze --- libs/bootstrap/src/setups/logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/bootstrap/src/setups/logger.ts b/libs/bootstrap/src/setups/logger.ts index f7e6810..c0456b3 100644 --- a/libs/bootstrap/src/setups/logger.ts +++ b/libs/bootstrap/src/setups/logger.ts @@ -94,7 +94,7 @@ export class LoggingInterceptor implements NestInterceptor { ); } - private sanitize(data: any): any { + private sanitize(data: T) { if (!data || typeof data !== 'object') return data; if (Array.isArray(data)) return data.map((v) => this.sanitize(v));