diff --git a/backend/package-lock.json b/backend/package-lock.json index 384099ec7..2d17c0221 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,13 +13,16 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/swagger": "^7.0.3", + "@nestjs/terminus": "^10.2.1", "@nestjs/testing": "^10.0.0", "@prisma/client": "^5.7.0", "dotenv": "^16.0.1", + "express-prom-bundle": "^7.0.0", "helmet": "^7.0.0", "nest-winston": "^1.9.4", "nestjs-prisma": "^0.22.0", "pg": "^8.11.3", + "prom-client": "^15.1.0", "reflect-metadata": "^0.2.0", "rimraf": "^5.0.0", "rxjs": "^7.8.0", @@ -1924,6 +1927,75 @@ } } }, + "node_modules/@nestjs/terminus": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/terminus/-/terminus-10.2.1.tgz", + "integrity": "sha512-23abPhotIP4+hrCZ8YkLEOmZ3m7eUYh1QOwdyrNkU9eMz/nc5LpVzy7jFbsNUuvlnT4MPV/7KXfyQTruQkTouw==", + "dependencies": { + "boxen": "5.1.2", + "check-disk-space": "3.4.0" + }, + "peerDependencies": { + "@grpc/grpc-js": "*", + "@grpc/proto-loader": "*", + "@mikro-orm/core": "*", + "@mikro-orm/nestjs": "*", + "@nestjs/axios": "^1.0.0 || ^2.0.0 || ^3.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "@nestjs/microservices": "^9.0.0 || ^10.0.0", + "@nestjs/mongoose": "^9.0.0 || ^10.0.0", + "@nestjs/sequelize": "^9.0.0 || ^10.0.0", + "@nestjs/typeorm": "^9.0.0 || ^10.0.0", + "@prisma/client": "*", + "mongoose": "*", + "reflect-metadata": "0.1.x", + "rxjs": "7.x", + "sequelize": "*", + "typeorm": "*" + }, + "peerDependenciesMeta": { + "@grpc/grpc-js": { + "optional": true + }, + "@grpc/proto-loader": { + "optional": true + }, + "@mikro-orm/core": { + "optional": true + }, + "@mikro-orm/nestjs": { + "optional": true + }, + "@nestjs/axios": { + "optional": true + }, + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/mongoose": { + "optional": true + }, + "@nestjs/sequelize": { + "optional": true + }, + "@nestjs/typeorm": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "mongoose": { + "optional": true + }, + "sequelize": { + "optional": true + }, + "typeorm": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.0.tgz", @@ -2002,6 +2074,14 @@ "npm": ">=5.0.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz", + "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2283,7 +2363,6 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -2293,7 +2372,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -2331,7 +2409,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -2343,7 +2420,6 @@ "version": "4.17.41", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", - "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -2363,8 +2439,7 @@ "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", @@ -2420,8 +2495,7 @@ "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/node": { "version": "20.11.5", @@ -2434,14 +2508,12 @@ "node_modules/@types/qs": { "version": "6.9.11", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", - "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==", - "dev": true + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "node_modules/@types/semver": { "version": "7.5.6", @@ -2453,7 +2525,6 @@ "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -2463,7 +2534,6 @@ "version": "1.15.5", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", - "dev": true, "dependencies": { "@types/http-errors": "*", "@types/mime": "*", @@ -2934,6 +3004,14 @@ } } }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dependencies": { + "string-width": "^4.1.0" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -3332,6 +3410,11 @@ "node": ">=8" } }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -3391,6 +3474,54 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -3618,6 +3749,14 @@ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, + "node_modules/check-disk-space": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.4.0.tgz", + "integrity": "sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==", + "engines": { + "node": ">=16" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -3673,6 +3812,17 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -5265,6 +5415,23 @@ "node": ">= 0.10.0" } }, + "node_modules/express-prom-bundle": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-7.0.0.tgz", + "integrity": "sha512-VwVaCyGBGHkHdecpTqRdW1Jm2fXK8weCUKjGjNWorc9g4M+cZ3xoj+N9uQzfRWfIPXJG5QOaiAziOIalQzMwgA==", + "dependencies": { + "@types/express": "^4.17.21", + "express": "^4.18.2", + "on-finished": "^2.3.0", + "url-value-parser": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "prom-client": ">=15.0.0" + } + }, "node_modules/express/node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -9116,6 +9283,18 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/prom-client": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.0.tgz", + "integrity": "sha512-cCD7jLTqyPdjEPBo/Xk4Iu8jxjuZgZJ3e/oET3L+ZwOuap/7Cw3dH/TJSsZKs1TQLZ2IHpIlRAKw82ef06kmMw==", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -10283,6 +10462,14 @@ "node": ">=6" } }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "dependencies": { + "bintrees": "1.0.2" + } + }, "node_modules/terser": { "version": "5.27.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", @@ -10685,7 +10872,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -10871,6 +11057,14 @@ "punycode": "^2.1.0" } }, + "node_modules/url-value-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/url-value-parser/-/url-value-parser-2.2.0.tgz", + "integrity": "sha512-yIQdxJpgkPamPPAPuGdS7Q548rLhny42tg8d4vyTNzFqvOnwqrgHXvgehT09U7fwrzxi3RxCiXjoNUNnNOlQ8A==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -11094,6 +11288,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/winston": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.11.0.tgz", diff --git a/backend/package.json b/backend/package.json index d46146fc6..2a3836b49 100644 --- a/backend/package.json +++ b/backend/package.json @@ -29,18 +29,21 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/swagger": "^7.0.3", + "@nestjs/terminus": "^10.2.1", "@nestjs/testing": "^10.0.0", "@prisma/client": "^5.7.0", "dotenv": "^16.0.1", + "express-prom-bundle": "^7.0.0", + "helmet": "^7.0.0", + "nest-winston": "^1.9.4", "nestjs-prisma": "^0.22.0", "pg": "^8.11.3", + "prom-client": "^15.1.0", "reflect-metadata": "^0.2.0", "rimraf": "^5.0.0", "rxjs": "^7.8.0", "swagger-ui-express": "^5.0.0", - "winston": "^3.11.0", - "nest-winston": "^1.9.4", - "helmet": "^7.0.0" + "winston": "^3.11.0" }, "devDependencies": { "@types/express": "^4.17.15", diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index e1145a703..c1947b34d 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -1,5 +1,6 @@ generator client { provider = "prisma-client-js" + previewFeatures = ["metrics"] } datasource db { diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index b7eb8991e..a198dff3a 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,11 +1,14 @@ import "dotenv/config"; -import { Logger, MiddlewareConsumer, Module } from "@nestjs/common"; +import { Logger, MiddlewareConsumer, Module, RequestMethod } from "@nestjs/common"; import { HTTPLoggerMiddleware } from "./middleware/req.res.logger"; import { loggingMiddleware, PrismaModule } from "nestjs-prisma"; import { ConfigModule } from "@nestjs/config"; import { UsersModule } from "./users/users.module"; import { AppService } from "./app.service"; import { AppController } from "./app.controller"; +import { MetricsController } from "./metrics.controller"; +import { TerminusModule } from '@nestjs/terminus'; +import { HealthController } from "./health.controller"; const DB_HOST = process.env.POSTGRES_HOST || "localhost"; const DB_USER = process.env.POSTGRES_USER || "postgres"; @@ -14,33 +17,41 @@ const DB_PORT = process.env.POSTGRES_PORT || 5432; const DB_NAME = process.env.POSTGRES_DATABASE || "postgres"; const DB_SCHEMA = process.env.DB_SCHEMA || "users"; +function getMiddlewares() { + if (process.env.PRISMA_LOGGING) { + return [ + // configure your prisma middleware + loggingMiddleware({ + logger: new Logger("PrismaMiddleware"), + logLevel: "debug" + }) + ]; + } + return []; +} + @Module({ imports: [ ConfigModule.forRoot(), + TerminusModule, PrismaModule.forRoot({ isGlobal: true, prismaServiceOptions:{ prismaOptions:{ - log: ["query", "info", "error", "warn"], + log: ["error", "warn"], errorFormat: "pretty", datasourceUrl: `postgresql://${DB_USER}:${DB_PWD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?schema=${DB_SCHEMA}&connection_limit=5`, }, - middlewares: [ - // configure your prisma middleware - loggingMiddleware({ - logger: new Logger("PrismaMiddleware"), - logLevel: "log" - }) - ] + middlewares: getMiddlewares(), }, }), UsersModule ], - controllers: [AppController], + controllers: [AppController,MetricsController, HealthController], providers: [AppService] }) export class AppModule { // let's add a middleware on all routes configure(consumer: MiddlewareConsumer) { - consumer.apply(HTTPLoggerMiddleware).forRoutes("*"); + consumer.apply(HTTPLoggerMiddleware).exclude({ path: 'metrics', method: RequestMethod.ALL }, { path: 'health', method: RequestMethod.ALL }).forRoutes('*'); } } diff --git a/backend/src/app.spec.ts b/backend/src/app.spec.ts index a44da5378..0f8c135b0 100644 --- a/backend/src/app.spec.ts +++ b/backend/src/app.spec.ts @@ -1,6 +1,18 @@ import {NestExpressApplication} from '@nestjs/platform-express'; import {bootstrap} from "./app"; - +jest.mock('prom-client', () => ({ + Registry: jest.fn().mockImplementation(() => ({ + })), + collectDefaultMetrics: jest.fn().mockImplementation(() => ({ + })), +})); +jest.mock('express-prom-bundle', () => ({ + default: jest.fn().mockImplementation(() => ({ + })), +})); +jest.mock('./prom', () => ({ + metricsMiddleware: jest.fn().mockImplementation((req, res, next) => next()), +})); describe('main', () => { let app: NestExpressApplication; diff --git a/backend/src/app.ts b/backend/src/app.ts index f77062abc..62d31899b 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -5,6 +5,7 @@ import { customLogger } from './common/logger.config'; import { NestExpressApplication } from '@nestjs/platform-express'; import helmet from 'helmet'; import { VersioningType } from '@nestjs/common'; +import { metricsMiddleware } from "./prom"; /** * @@ -17,6 +18,7 @@ export async function bootstrap() { app.use(helmet()); app.enableCors(); app.set("trust proxy", 1); + app.use(metricsMiddleware); app.enableShutdownHooks(); app.setGlobalPrefix("api"); app.enableVersioning({ diff --git a/backend/src/health.controller.ts b/backend/src/health.controller.ts new file mode 100644 index 000000000..c58c3a83e --- /dev/null +++ b/backend/src/health.controller.ts @@ -0,0 +1,19 @@ +import { Controller, Get } from "@nestjs/common"; +import { HealthCheckService, HealthCheck, PrismaHealthIndicator } from "@nestjs/terminus"; +import { PrismaService } from "nestjs-prisma"; +@Controller("health") +export class HealthController { + constructor( + private health: HealthCheckService, + private prisma: PrismaHealthIndicator, + private readonly prismaService: PrismaService, + ) {} + + @Get() + @HealthCheck() + check() { + return this.health.check([ + () => this.prisma.pingCheck('prisma', this.prismaService), + ]); + } +} diff --git a/backend/src/metrics.controller.ts b/backend/src/metrics.controller.ts new file mode 100644 index 000000000..62bc71d21 --- /dev/null +++ b/backend/src/metrics.controller.ts @@ -0,0 +1,15 @@ +import { Controller, Get, Res } from "@nestjs/common"; +import { Response } from "express"; +import { register } from "./prom"; +import { PrismaService } from "nestjs-prisma"; +@Controller("metrics") +export class MetricsController { + constructor(private prisma: PrismaService) {} + + @Get() + async getMetrics(@Res() res: Response) { + const prismaMetrics = await this.prisma.$metrics.prometheus(); + const appMetrics = await register.metrics(); + res.end(prismaMetrics + appMetrics); + } +} diff --git a/backend/src/middleware/req.res.logger.ts b/backend/src/middleware/req.res.logger.ts index efff5a160..7ce4d43bb 100644 --- a/backend/src/middleware/req.res.logger.ts +++ b/backend/src/middleware/req.res.logger.ts @@ -10,7 +10,7 @@ export class HTTPLoggerMiddleware implements NestMiddleware { response.on("finish", () => { const { statusCode } = response; - const contentLength = response.get("content-length"); + const contentLength = response.get("content-length") || '-'; const hostedHttpLogFormat = `${method} ${originalUrl} ${statusCode} ${contentLength} - ${request.get( "user-agent" )}`; diff --git a/backend/src/prom.ts b/backend/src/prom.ts new file mode 100644 index 000000000..5e3b9a231 --- /dev/null +++ b/backend/src/prom.ts @@ -0,0 +1,11 @@ +import * as prom from 'prom-client'; +import promBundle from 'express-prom-bundle'; +const register = new prom.Registry(); +prom.collectDefaultMetrics({ register }); +const metricsMiddleware = promBundle({ + includeMethod: true, + includePath: true, + metricsPath: '/prom-metrics', + promRegistry: register, +}); +export { metricsMiddleware, register }; diff --git a/charts/quickstart-openshift/values.yaml b/charts/quickstart-openshift/values.yaml index e32e909d1..a1516e2d1 100644 --- a/charts/quickstart-openshift/values.yaml +++ b/charts/quickstart-openshift/values.yaml @@ -79,7 +79,7 @@ backend: #-- the readiness probe for the container. it is optional and is an object. for default values check this link: https://github.com/bcgov/helm-service/blob/main/charts/component/templates/deployment.yaml#L312-L316 readinessProbe: httpGet: - path: /api + path: /api/health port: 3000 scheme: HTTP initialDelaySeconds: 5 @@ -92,7 +92,7 @@ backend: successThreshold: 1 failureThreshold: 3 httpGet: - path: /api + path: /api/health port: 3000 scheme: HTTP initialDelaySeconds: 15 @@ -208,7 +208,10 @@ backend: nodeSelector: { } tolerations: [ ] affinity: { } - + podAnnotations: | + prometheus.io/scrape: 'true' + prometheus.io/port: '3000' + prometheus.io/path: '/api/metrics' frontend: # -- enable or disable a component deployment. enabled: true @@ -276,6 +279,9 @@ frontend: - name: http2 containerPort: 3001 protocol: TCP + - name: metrics + containerPort: 3003 + protocol: TCP #-- the resources for the container. it is optional and is an object. for default values check this link: https://github.com/bcgov/helm-service/blob/main/charts/component/templates/deployment.yaml#L298-L304 resources: limits: @@ -306,6 +312,10 @@ frontend: initialDelaySeconds: 15 periodSeconds: 30 timeoutSeconds: 5 + podAnnotations: | + prometheus.io/scrape: 'true' + prometheus.io/port: '3003' + prometheus.io/path: '/metrics' #-- autoscaling for the component. it is optional and is an object. autoscaling: #-- enable or disable autoscaling. @@ -353,6 +363,10 @@ frontend: targetPort: 3000 #-- the protocol for the port. it can be TCP or UDP. TCP is the default and is recommended. protocol: TCP + - port: 3003 + targetPort: 3003 + protocol: TCP + name: metrics #-- the route for the component. it is optional and is an object. To make the application accessible from internet, use the route. route: #-- enable or disable the route. diff --git a/frontend/Caddyfile b/frontend/Caddyfile index 2a88e9c49..4b04c9044 100644 --- a/frontend/Caddyfile +++ b/frontend/Caddyfile @@ -1,6 +1,9 @@ { auto_https off - admin off + admin 0.0.0.0:3003 + servers { + metrics + } } :3000 { log {