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/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..f4a1146 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'; @@ -23,19 +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 { ZodValidationInterceptor } from '@shared/interceptors'; +import { MetricsModule } from '@libs/metrics'; @Module({ imports: [ ConfigModule, - PrometheusModule.registerAsync({ - useFactory: () => ({ - path: 'dump', - defaultMetrics: { - enabled: process.env.NODE_ENV !== 'test', - }, - }), - }), DatabaseModule.registerAsync({ global: true, inject: [ConfigService], @@ -65,6 +57,7 @@ import { ZodValidationInterceptor } from '@shared/interceptors/zod-validation.in UserModule, TeamsModule, ProjectsModule, + MetricsModule, BullBoardModule.forRoot({ route: '/queues', boardOptions: { 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, 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/*"],