diff --git a/compose.yml b/compose.yml index 3a591e5..77b15fb 100644 --- a/compose.yml +++ b/compose.yml @@ -9,11 +9,11 @@ services: restart: always env_file: - .env.production.local + expose: + - '9101' networks: - teacinema - - networks: teacinema: external: true diff --git a/package.json b/package.json index 8945c96..edadb4f 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@teacinema/common": "^1.0.0", "@teacinema/contracts": "^1.0.0", "@teacinema/passport": "^1.0.0", + "@willsoto/nestjs-prometheus": "^6.0.2", "amqp-connection-manager": "^5.0.0", "amqplib": "^0.10.9", "class-transformer": "^0.5.1", @@ -42,6 +43,7 @@ "dotenv-expand": "^12.0.3", "ioredis": "^5.9.2", "prisma": "^7.3.0", + "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, diff --git a/src/app.module.ts b/src/app.module.ts index ad24d39..75cc7fe 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,6 +1,8 @@ import { Module } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' +import { ObservabilityModule } from '@/observability/observability.module' + import { dabataseEnv, grpcEnv, @@ -32,6 +34,7 @@ import { UsersModule } from './modules/users/users.module' }), PrismaModule, RedisModule, + ObservabilityModule, AuthModule, OtpModule, UsersModule, diff --git a/src/main.ts b/src/main.ts index 2dbdaaa..c5e0dc0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,6 +12,6 @@ async function bootstrap() { createGrpcServer(app, config) await app.startAllMicroservices() - await app.init() + await app.listen(9101) } bootstrap() diff --git a/src/observability/metrics/grpc-metrics.interceptor.ts b/src/observability/metrics/grpc-metrics.interceptor.ts new file mode 100644 index 0000000..8137760 --- /dev/null +++ b/src/observability/metrics/grpc-metrics.interceptor.ts @@ -0,0 +1,55 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor +} from '@nestjs/common' +import { InjectMetric } from '@willsoto/nestjs-prometheus' +import { Counter, Histogram } from 'prom-client' +import { Observable, tap } from 'rxjs' + +@Injectable() +export class GrpcMetricsInterceptor implements NestInterceptor { + private readonly SERVICE_NAME: string + + public constructor( + @InjectMetric('grpc_request_duration_seconds') + private readonly histogram: Histogram, + @InjectMetric('grpc_request_total') + private readonly counter: Counter + ) { + this.SERVICE_NAME = 'auth-service' + } + + public intercept( + context: ExecutionContext, + next: CallHandler + ): Observable { + const handler = context.getHandler() + const entTimer = this.histogram.startTimer({ + service: this.SERVICE_NAME, + method: handler.name + }) + + return next.handle().pipe( + tap({ + next: () => { + this.counter.inc({ + service: this.SERVICE_NAME, + method: handler.name, + status: 'OK' + }) + entTimer() + }, + error: () => { + this.counter.inc({ + service: this.SERVICE_NAME, + method: handler.name, + status: 'ERROR' + }) + entTimer() + } + }) + ) + } +} diff --git a/src/observability/metrics/metrics.module.ts b/src/observability/metrics/metrics.module.ts new file mode 100644 index 0000000..da01423 --- /dev/null +++ b/src/observability/metrics/metrics.module.ts @@ -0,0 +1,40 @@ +import { Global, Module } from '@nestjs/common' +import { APP_INTERCEPTOR } from '@nestjs/core' +import { + makeCounterProvider, + makeHistogramProvider, + PrometheusModule +} from '@willsoto/nestjs-prometheus' + +import { GrpcMetricsInterceptor } from './grpc-metrics.interceptor' + +@Global() +@Module({ + imports: [ + PrometheusModule.register({ + path: '/metrics', + defaultMetrics: { + enabled: true + } + }) + ], + providers: [ + makeHistogramProvider({ + name: 'grpc_request_duration_seconds', + help: 'GRPC request latency', + labelNames: ['service', 'method'], + buckets: [0.05, 0.1, 0.2, 0.3, 0.5, 1, 1.5, 2, 3, 5] + }), + makeCounterProvider({ + name: 'grpc_request_total', + help: 'Total grpc requests', + labelNames: ['service', 'method', 'status'] + }), + GrpcMetricsInterceptor, + { + provide: APP_INTERCEPTOR, + useClass: GrpcMetricsInterceptor + } + ] +}) +export class MetricsModule {} diff --git a/src/observability/observability.module.ts b/src/observability/observability.module.ts new file mode 100644 index 0000000..857e458 --- /dev/null +++ b/src/observability/observability.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common' + +import { MetricsModule } from './metrics/metrics.module' + +@Module({ + imports: [MetricsModule] +}) +export class ObservabilityModule {} diff --git a/yarn.lock b/yarn.lock index 17ddc03..a63b83c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1197,6 +1197,11 @@ dependencies: consola "^3.2.3" +"@opentelemetry/api@^1.4.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" + integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== + "@paralleldrive/cuid2@^2.2.2": version "2.3.1" resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz#3d62ea9e7be867d3fa94b9897fab5b0ae187d784" @@ -2039,6 +2044,11 @@ "@webassemblyjs/ast" "1.14.1" "@xtuc/long" "4.2.2" +"@willsoto/nestjs-prometheus@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@willsoto/nestjs-prometheus/-/nestjs-prometheus-6.0.2.tgz#d603764a923848442ed092411716c0bf211de01f" + integrity sha512-ePyLZYdIrOOdlOWovzzMisIgviXqhPVzFpSMKNNhn6xajhRHeBsjAzSdpxZTc6pnjR9hw1lNAHyKnKl7lAPaVg== + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -2309,6 +2319,11 @@ baseline-browser-mapping@^2.9.0: resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz#6baaa0069883f50a99cdb31b56646491f47c05d7" integrity sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg== +bintrees@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.2.tgz#49f896d6e858a4a499df85c38fb399b9aff840f8" + integrity sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw== + bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -4968,6 +4983,14 @@ prisma@^7.3.0: mysql2 "3.15.3" postgres "3.4.7" +prom-client@^15.1.3: + version "15.1.3" + resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-15.1.3.tgz#69fa8de93a88bc9783173db5f758dc1c69fa8fc2" + integrity sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g== + dependencies: + "@opentelemetry/api" "^1.4.0" + tdigest "^0.1.1" + promise-breaker@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/promise-breaker/-/promise-breaker-6.0.0.tgz#107d2b70f161236abdb4ac5a736c7eb8df489d0f" @@ -5554,6 +5577,13 @@ tapable@^2.2.0, tapable@^2.2.1, tapable@^2.3.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6" integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg== +tdigest@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.2.tgz#96c64bac4ff10746b910b0e23b515794e12faced" + integrity sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA== + dependencies: + bintrees "1.0.2" + terser-webpack-plugin@^5.3.16: version "5.3.16" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz#741e448cc3f93d8026ebe4f7ef9e4afacfd56330"