From 264bf1bf266685fdcab17e3c3a096b2f694e91e5 Mon Sep 17 00:00:00 2001 From: Maxim Date: Wed, 20 May 2026 17:01:31 +0300 Subject: [PATCH 1/3] feat(app): add SkipResponseValidation decorator to bypass response validation --- src/shared/decorators/index.ts | 1 + .../decorators/skip-response-validation.decorator.ts | 4 ++++ .../interceptors/zod-validation.interceptor.ts | 12 ++++++++++++ 3 files changed, 17 insertions(+) create mode 100644 src/shared/decorators/skip-response-validation.decorator.ts diff --git a/src/shared/decorators/index.ts b/src/shared/decorators/index.ts index baf933f..132aa07 100644 --- a/src/shared/decorators/index.ts +++ b/src/shared/decorators/index.ts @@ -1,3 +1,4 @@ export { ApiBaseController } from './api-controller.decorator'; export { IS_PUBLIC_KEY, Public } from './public.decorator'; export * from './user.decorator'; +export { SkipResponseValidation } from './skip-response-validation.decorator'; diff --git a/src/shared/decorators/skip-response-validation.decorator.ts b/src/shared/decorators/skip-response-validation.decorator.ts new file mode 100644 index 0000000..c8c2225 --- /dev/null +++ b/src/shared/decorators/skip-response-validation.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const SKIP_RESPONSE_VALIDATION_KEY = 'SKIP_RESPONSE_VALIDATION_KEY'; +export const SkipResponseValidation = () => SetMetadata(SKIP_RESPONSE_VALIDATION_KEY, true); diff --git a/src/shared/interceptors/zod-validation.interceptor.ts b/src/shared/interceptors/zod-validation.interceptor.ts index 54b4cd5..6ab1781 100644 --- a/src/shared/interceptors/zod-validation.interceptor.ts +++ b/src/shared/interceptors/zod-validation.interceptor.ts @@ -9,6 +9,7 @@ import { Reflector } from '@nestjs/core'; import { map, Observable } from 'rxjs'; import { BaseException } from '@shared/error'; import { z } from 'zod/v4'; +import { SKIP_RESPONSE_VALIDATION_KEY } from '@shared/decorators/skip-response-validation.decorator'; export const ZOD_RESPONSE_TOKEN = 'ZOD_RESPONSE_TOKEN'; @@ -18,6 +19,17 @@ export class ZodValidationInterceptor implements NestInterceptor): Observable { const handler = context.getHandler(); + const controller = context.getClass(); + + const isSkipped = this.reflector.getAllAndOverride(SKIP_RESPONSE_VALIDATION_KEY, [ + handler, + controller, + ]); + + if (isSkipped) { + return next.handle(); + } + const metadata = this.reflector.get<{ schema: z.ZodTypeAny } | undefined>( ZOD_RESPONSE_TOKEN, handler, From 6419bb1e9748203dc68eaa1114cbb052d03ef869 Mon Sep 17 00:00:00 2001 From: Maxim Date: Wed, 20 May 2026 17:04:06 +0300 Subject: [PATCH 2/3] feat(metrics): add MetricsModule with endpoint for Prometheus metrics --- libs/metrics/metrics.controller.ts | 13 +++++++++++++ libs/metrics/metrics.module.ts | 17 +++++++++++++++++ package.json | 1 + pnpm-lock.yaml | 3 +++ src/app.module.ts | 11 ++--------- 5 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 libs/metrics/metrics.controller.ts create mode 100644 libs/metrics/metrics.module.ts diff --git a/libs/metrics/metrics.controller.ts b/libs/metrics/metrics.controller.ts new file mode 100644 index 0000000..b3267fd --- /dev/null +++ b/libs/metrics/metrics.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get, Header } from '@nestjs/common'; +import * as client from 'prom-client'; +import { SkipResponseValidation } from '@shared/decorators'; + +@Controller() +export class MetricsController { + @Get('dump') + @Header('Content-Type', client.register.contentType) + @SkipResponseValidation() + async getMetrics() { + return client.register.metrics(); + } +} diff --git a/libs/metrics/metrics.module.ts b/libs/metrics/metrics.module.ts new file mode 100644 index 0000000..b1a04ef --- /dev/null +++ b/libs/metrics/metrics.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { PrometheusModule } from '@willsoto/nestjs-prometheus'; +import { MetricsController } from './metrics.controller'; + +@Module({ + imports: [ + PrometheusModule.registerAsync({ + useFactory: () => ({ + defaultMetrics: { + enabled: process.env.NODE_ENV !== 'test', + }, + }), + }), + ], + controllers: [MetricsController], +}) +export class MetricsModule {} diff --git a/package.json b/package.json index 3d4302f..c0f4bd2 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "passport": "^0.7.0", "passport-jwt": "^4.0.1", "postgres": "^3.4.9", + "prom-client": "^15.1.3", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "transliteration": "^2.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef2954e..44f45a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,9 @@ importers: postgres: specifier: ^3.4.9 version: 3.4.9 + prom-client: + specifier: ^15.1.3 + version: 15.1.3 reflect-metadata: specifier: ^0.2.0 version: 0.2.2 diff --git a/src/app.module.ts b/src/app.module.ts index 538199e..c646c24 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,7 +5,6 @@ import { ConfigService } from '@nestjs/config'; import * as schema from './shared/entities'; import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { ZodValidationPipe } from 'nestjs-zod'; -import { PrometheusModule } from '@willsoto/nestjs-prometheus'; import { HealthModule } from '@libs/health'; import { UserModule } from './user'; import { GlobalExceptionFilter } from '@shared/error'; @@ -24,18 +23,12 @@ import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; import { ICacheService } from '@shared/adapters/cache/ports'; import { DatabaseHealthService } from '@libs/database'; import { ZodValidationInterceptor } from '@shared/interceptors/zod-validation.interceptor'; +import { MetricsModule } from '../libs/metrics/metrics.module'; @Module({ imports: [ ConfigModule, - PrometheusModule.registerAsync({ - useFactory: () => ({ - path: 'dump', - defaultMetrics: { - enabled: process.env.NODE_ENV !== 'test', - }, - }), - }), + MetricsModule, DatabaseModule.registerAsync({ global: true, inject: [ConfigService], From 7ed8727650844429b350a7161aee5806ab78b300 Mon Sep 17 00:00:00 2001 From: soorq Date: Sun, 31 May 2026 19:54:42 +0300 Subject: [PATCH 3/3] refactor: enchance metrics bump module, implement global path --- libs/metrics/metrics.controller.ts | 13 ------------- libs/metrics/metrics.module.ts | 17 ----------------- libs/metrics/src/index.ts | 1 + libs/metrics/src/metrics.controller.ts | 25 +++++++++++++++++++++++++ libs/metrics/src/metrics.module.ts | 15 +++++++++++++++ libs/metrics/tsconfig.lib.json | 9 +++++++++ nest-cli.json | 9 +++++++++ src/app.module.ts | 6 +++--- tsconfig.json | 2 ++ 9 files changed, 64 insertions(+), 33 deletions(-) delete mode 100644 libs/metrics/metrics.controller.ts delete mode 100644 libs/metrics/metrics.module.ts create mode 100644 libs/metrics/src/index.ts create mode 100644 libs/metrics/src/metrics.controller.ts create mode 100644 libs/metrics/src/metrics.module.ts create mode 100644 libs/metrics/tsconfig.lib.json diff --git a/libs/metrics/metrics.controller.ts b/libs/metrics/metrics.controller.ts deleted file mode 100644 index b3267fd..0000000 --- a/libs/metrics/metrics.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get, Header } from '@nestjs/common'; -import * as client from 'prom-client'; -import { SkipResponseValidation } from '@shared/decorators'; - -@Controller() -export class MetricsController { - @Get('dump') - @Header('Content-Type', client.register.contentType) - @SkipResponseValidation() - async getMetrics() { - return client.register.metrics(); - } -} diff --git a/libs/metrics/metrics.module.ts b/libs/metrics/metrics.module.ts deleted file mode 100644 index b1a04ef..0000000 --- a/libs/metrics/metrics.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Module } from '@nestjs/common'; -import { PrometheusModule } from '@willsoto/nestjs-prometheus'; -import { MetricsController } from './metrics.controller'; - -@Module({ - imports: [ - PrometheusModule.registerAsync({ - useFactory: () => ({ - defaultMetrics: { - enabled: process.env.NODE_ENV !== 'test', - }, - }), - }), - ], - controllers: [MetricsController], -}) -export class MetricsModule {} diff --git a/libs/metrics/src/index.ts b/libs/metrics/src/index.ts new file mode 100644 index 0000000..3841f24 --- /dev/null +++ b/libs/metrics/src/index.ts @@ -0,0 +1 @@ +export * from './metrics.module'; diff --git a/libs/metrics/src/metrics.controller.ts b/libs/metrics/src/metrics.controller.ts new file mode 100644 index 0000000..e4ee172 --- /dev/null +++ b/libs/metrics/src/metrics.controller.ts @@ -0,0 +1,25 @@ +import { Controller, Get, Header } from '@nestjs/common'; +import { SkipResponseValidation } from '@shared/decorators'; +import * as client from 'prom-client'; + +@Controller('metrics') +export class MetricsController { + @Get('system') + @Header('Content-Type', client.register.contentType) + @SkipResponseValidation() + async getMetrics() { + return client.register.metrics(); + } + + /** TODO: добавить, чтоб тут была сборка http запросов + * почитай, как сделать правильно, там есть метка le + * образно, /v1/users/me 10ms le=29 + * То есть это системная метрика, где контекст для приложения + + * Как седалешь удалить этот комментарий! + */ + @Get() + @Header('Content-Type', client.register.contentType) + @SkipResponseValidation() + async getHttp() {} +} diff --git a/libs/metrics/src/metrics.module.ts b/libs/metrics/src/metrics.module.ts new file mode 100644 index 0000000..2812803 --- /dev/null +++ b/libs/metrics/src/metrics.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { PrometheusModule } from '@willsoto/nestjs-prometheus'; +import { MetricsController } from './metrics.controller'; + +@Module({ + imports: [ + PrometheusModule.register({ + controller: MetricsController, + defaultMetrics: { + enabled: process.env.NODE_ENV !== 'test', + }, + }), + ], +}) +export class MetricsModule {} diff --git a/libs/metrics/tsconfig.lib.json b/libs/metrics/tsconfig.lib.json new file mode 100644 index 0000000..c821435 --- /dev/null +++ b/libs/metrics/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/metrics" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/nest-cli.json b/nest-cli.json index 110f81c..631e79c 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -60,6 +60,15 @@ "compilerOptions": { "tsConfigPath": "libs/imagor/tsconfig.lib.json" } + }, + "metrics": { + "type": "library", + "root": "libs/metrics", + "entryFile": "index", + "sourceRoot": "libs/metrics/src", + "compilerOptions": { + "tsConfigPath": "libs/metrics/tsconfig.lib.json" + } } } } diff --git a/src/app.module.ts b/src/app.module.ts index c646c24..f4a1146 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -22,13 +22,12 @@ import { S3Service } from '@libs/s3'; import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; import { ICacheService } from '@shared/adapters/cache/ports'; import { DatabaseHealthService } from '@libs/database'; -import { ZodValidationInterceptor } from '@shared/interceptors/zod-validation.interceptor'; -import { MetricsModule } from '../libs/metrics/metrics.module'; +import { ZodValidationInterceptor } from '@shared/interceptors'; +import { MetricsModule } from '@libs/metrics'; @Module({ imports: [ ConfigModule, - MetricsModule, DatabaseModule.registerAsync({ global: true, inject: [ConfigService], @@ -58,6 +57,7 @@ import { MetricsModule } from '../libs/metrics/metrics.module'; UserModule, TeamsModule, ProjectsModule, + MetricsModule, BullBoardModule.forRoot({ route: '/queues', boardOptions: { diff --git a/tsconfig.json b/tsconfig.json index 4884f1b..21038d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,8 @@ "@libs/health/*": ["./libs/health/src/*"], "@libs/imagor": ["./libs/imagor/src"], "@libs/imagor/*": ["./libs/imagor/src/*"], + "@libs/metrics": ["./libs/metrics/src"], + "@libs/metrics/*": ["./libs/metrics/src/*"], "@libs/s3": ["./libs/s3/src"], "@libs/s3/*": ["./libs/s3/src/*"], "@shared/*": ["./src/shared/*"],