feat(api): add health check endpoints with @nestjs/terminus
Add HealthModule with /health (liveness) and /ready (readiness) probes. Readiness checks DB (Prisma) and Redis connectivity. Replaces the basic /health endpoint in AppController. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.0",
|
||||
"@nestjs/swagger": "^11.2.6",
|
||||
"@nestjs/terminus": "^11.1.1",
|
||||
"@nestjs/throttler": "^6.5.0",
|
||||
"@paralleldrive/cuid2": "^3.3.0",
|
||||
"@prisma/adapter-pg": "^7.7.0",
|
||||
|
||||
@@ -6,9 +6,4 @@ export class AppController {
|
||||
root() {
|
||||
return { status: 'ok', service: 'goodgo-api' };
|
||||
}
|
||||
|
||||
@Get('health')
|
||||
healthCheck() {
|
||||
return { status: 'ok', service: 'goodgo-api', timestamp: new Date().toISOString() };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup';
|
||||
import { AdminModule } from '@modules/admin';
|
||||
import { AnalyticsModule } from '@modules/analytics';
|
||||
import { AuthModule } from '@modules/auth';
|
||||
import { HealthModule } from '@modules/health';
|
||||
import { ListingsModule } from '@modules/listings';
|
||||
import { McpIntegrationModule } from '@modules/mcp';
|
||||
import { HttpMetricsInterceptor, MetricsModule } from '@modules/metrics';
|
||||
@@ -25,6 +26,7 @@ import { AppController } from './app.controller';
|
||||
SentryModule.forRoot(),
|
||||
CqrsModule.forRoot(),
|
||||
SharedModule,
|
||||
HealthModule,
|
||||
AuthModule,
|
||||
ListingsModule,
|
||||
ReviewsModule,
|
||||
|
||||
33
apps/api/src/modules/health/health.controller.ts
Normal file
33
apps/api/src/modules/health/health.controller.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
import { HealthCheck, HealthCheckService } from '@nestjs/terminus';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
import { PrismaHealthIndicator } from './infrastructure/prisma.health';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
import { RedisHealthIndicator } from './infrastructure/redis.health';
|
||||
|
||||
@Controller()
|
||||
export class HealthController {
|
||||
constructor(
|
||||
private readonly health: HealthCheckService,
|
||||
private readonly prismaHealth: PrismaHealthIndicator,
|
||||
private readonly redisHealth: RedisHealthIndicator,
|
||||
) {}
|
||||
|
||||
/** Liveness probe — always returns 200 if the process is running */
|
||||
@Get('health')
|
||||
@HealthCheck()
|
||||
liveness() {
|
||||
return this.health.check([]);
|
||||
}
|
||||
|
||||
/** Readiness probe — checks DB and Redis connectivity */
|
||||
@Get('ready')
|
||||
@HealthCheck()
|
||||
readiness() {
|
||||
return this.health.check([
|
||||
() => this.prismaHealth.isHealthy('database'),
|
||||
() => this.redisHealth.isHealthy('redis'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
12
apps/api/src/modules/health/health.module.ts
Normal file
12
apps/api/src/modules/health/health.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TerminusModule } from '@nestjs/terminus';
|
||||
import { HealthController } from './health.controller';
|
||||
import { PrismaHealthIndicator } from './infrastructure/prisma.health';
|
||||
import { RedisHealthIndicator } from './infrastructure/redis.health';
|
||||
|
||||
@Module({
|
||||
imports: [TerminusModule],
|
||||
controllers: [HealthController],
|
||||
providers: [PrismaHealthIndicator, RedisHealthIndicator],
|
||||
})
|
||||
export class HealthModule {}
|
||||
1
apps/api/src/modules/health/index.ts
Normal file
1
apps/api/src/modules/health/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { HealthModule } from './health.module';
|
||||
20
apps/api/src/modules/health/infrastructure/prisma.health.ts
Normal file
20
apps/api/src/modules/health/infrastructure/prisma.health.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { HealthCheckError, HealthIndicator, type HealthIndicatorResult } from '@nestjs/terminus';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
import { PrismaService } from '@modules/shared/infrastructure/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaHealthIndicator extends HealthIndicator {
|
||||
constructor(private readonly prisma: PrismaService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async isHealthy(key: string): Promise<HealthIndicatorResult> {
|
||||
try {
|
||||
await this.prisma.$queryRawUnsafe('SELECT 1');
|
||||
return this.getStatus(key, true);
|
||||
} catch {
|
||||
throw new HealthCheckError('Database check failed', this.getStatus(key, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
25
apps/api/src/modules/health/infrastructure/redis.health.ts
Normal file
25
apps/api/src/modules/health/infrastructure/redis.health.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { HealthCheckError, HealthIndicator, type HealthIndicatorResult } from '@nestjs/terminus';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
import { RedisService } from '@modules/shared/infrastructure/redis.service';
|
||||
|
||||
@Injectable()
|
||||
export class RedisHealthIndicator extends HealthIndicator {
|
||||
constructor(private readonly redis: RedisService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async isHealthy(key: string): Promise<HealthIndicatorResult> {
|
||||
try {
|
||||
const client = this.redis.getClient();
|
||||
const pong = await client.ping();
|
||||
const isHealthy = pong === 'PONG';
|
||||
const result = this.getStatus(key, isHealthy);
|
||||
if (isHealthy) return result;
|
||||
throw new HealthCheckError('Redis ping failed', result);
|
||||
} catch (error) {
|
||||
if (error instanceof HealthCheckError) throw error;
|
||||
throw new HealthCheckError('Redis check failed', this.getStatus(key, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
118
pnpm-lock.yaml
generated
118
pnpm-lock.yaml
generated
@@ -102,6 +102,9 @@ importers:
|
||||
'@nestjs/swagger':
|
||||
specifier: ^11.2.6
|
||||
version: 11.2.6(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)
|
||||
'@nestjs/terminus':
|
||||
specifier: ^11.1.1
|
||||
version: 11.1.1(@grpc/grpc-js@1.14.3)(@grpc/proto-loader@0.8.0)(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.2))(typescript@6.0.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
'@nestjs/throttler':
|
||||
specifier: ^6.5.0
|
||||
version: 6.5.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)
|
||||
@@ -1366,6 +1369,54 @@ packages:
|
||||
class-validator:
|
||||
optional: true
|
||||
|
||||
'@nestjs/terminus@11.1.1':
|
||||
resolution: {integrity: sha512-Ssql79H+EQY/Wg108eJqN4NiNsO/tLrj+qbzOWSQUf2JE4vJQ2RG3WTqUOrYjfjWmVHD3+Ys0+azed7LSMKScw==}
|
||||
peerDependencies:
|
||||
'@grpc/grpc-js': '*'
|
||||
'@grpc/proto-loader': '*'
|
||||
'@mikro-orm/core': '*'
|
||||
'@mikro-orm/nestjs': '*'
|
||||
'@nestjs/axios': ^2.0.0 || ^3.0.0 || ^4.0.0
|
||||
'@nestjs/common': ^10.0.0 || ^11.0.0
|
||||
'@nestjs/core': ^10.0.0 || ^11.0.0
|
||||
'@nestjs/microservices': ^10.0.0 || ^11.0.0
|
||||
'@nestjs/mongoose': ^11.0.0
|
||||
'@nestjs/sequelize': ^10.0.0 || ^11.0.0
|
||||
'@nestjs/typeorm': ^10.0.0 || ^11.0.0
|
||||
'@prisma/client': '*'
|
||||
mongoose: '*'
|
||||
reflect-metadata: 0.1.x || 0.2.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
|
||||
|
||||
'@nestjs/testing@11.1.18':
|
||||
resolution: {integrity: sha512-frzwNlpBgtAzI3hp/qo57DZoRO4RMTH1wST3QUYEhRTHyfPkLpzkWz3jV/mhApXjD0yT56Ptlzn6zuYPLh87Lw==}
|
||||
peerDependencies:
|
||||
@@ -3040,6 +3091,9 @@ packages:
|
||||
ajv@8.18.0:
|
||||
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
|
||||
|
||||
ansi-align@3.0.1:
|
||||
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
|
||||
|
||||
ansi-colors@4.1.3:
|
||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -3182,6 +3236,10 @@ packages:
|
||||
bowser@2.14.1:
|
||||
resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==}
|
||||
|
||||
boxen@5.1.2:
|
||||
resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
brace-expansion@1.1.13:
|
||||
resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==}
|
||||
|
||||
@@ -3239,6 +3297,10 @@ packages:
|
||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
camelcase@6.3.0:
|
||||
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
caniuse-lite@1.0.30001786:
|
||||
resolution: {integrity: sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==}
|
||||
|
||||
@@ -3260,6 +3322,10 @@ packages:
|
||||
cheap-ruler@4.0.0:
|
||||
resolution: {integrity: sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==}
|
||||
|
||||
check-disk-space@3.4.0:
|
||||
resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
@@ -3290,6 +3356,10 @@ packages:
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
cli-boxes@2.2.1:
|
||||
resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
cli-cursor@3.1.0:
|
||||
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -5855,6 +5925,10 @@ packages:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
type-fest@0.20.2:
|
||||
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
type-fest@0.7.1:
|
||||
resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -6141,6 +6215,10 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
widest-line@3.1.0:
|
||||
resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
word-wrap@1.2.5:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -7567,6 +7645,19 @@ snapshots:
|
||||
class-transformer: 0.5.1
|
||||
class-validator: 0.15.1
|
||||
|
||||
'@nestjs/terminus@11.1.1(@grpc/grpc-js@1.14.3)(@grpc/proto-loader@0.8.0)(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.2))(typescript@6.0.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)':
|
||||
dependencies:
|
||||
'@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
'@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
boxen: 5.1.2
|
||||
check-disk-space: 3.4.0
|
||||
reflect-metadata: 0.2.2
|
||||
rxjs: 7.8.2
|
||||
optionalDependencies:
|
||||
'@grpc/grpc-js': 1.14.3
|
||||
'@grpc/proto-loader': 0.8.0
|
||||
'@prisma/client': 7.7.0(prisma@7.7.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.2))(typescript@6.0.2)
|
||||
|
||||
'@nestjs/testing@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/platform-express@11.1.18)':
|
||||
dependencies:
|
||||
'@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
@@ -9510,6 +9601,10 @@ snapshots:
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
ansi-align@3.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
|
||||
ansi-colors@4.1.3: {}
|
||||
|
||||
ansi-escapes@7.3.0:
|
||||
@@ -9635,6 +9730,17 @@ snapshots:
|
||||
|
||||
bowser@2.14.1: {}
|
||||
|
||||
boxen@5.1.2:
|
||||
dependencies:
|
||||
ansi-align: 3.0.1
|
||||
camelcase: 6.3.0
|
||||
chalk: 4.1.2
|
||||
cli-boxes: 2.2.1
|
||||
string-width: 4.2.3
|
||||
type-fest: 0.20.2
|
||||
widest-line: 3.1.0
|
||||
wrap-ansi: 7.0.0
|
||||
|
||||
brace-expansion@1.1.13:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
@@ -9700,6 +9806,8 @@ snapshots:
|
||||
|
||||
camelcase-css@2.0.1: {}
|
||||
|
||||
camelcase@6.3.0: {}
|
||||
|
||||
caniuse-lite@1.0.30001786: {}
|
||||
|
||||
chai@6.2.2: {}
|
||||
@@ -9717,6 +9825,8 @@ snapshots:
|
||||
|
||||
cheap-ruler@4.0.0: {}
|
||||
|
||||
check-disk-space@3.4.0: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
dependencies:
|
||||
anymatch: 3.1.3
|
||||
@@ -9755,6 +9865,8 @@ snapshots:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
||||
cli-boxes@2.2.1: {}
|
||||
|
||||
cli-cursor@3.1.0:
|
||||
dependencies:
|
||||
restore-cursor: 3.1.0
|
||||
@@ -12473,6 +12585,8 @@ snapshots:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
type-fest@0.20.2: {}
|
||||
|
||||
type-fest@0.7.1: {}
|
||||
|
||||
type-fest@5.5.0:
|
||||
@@ -12790,6 +12904,10 @@ snapshots:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
|
||||
widest-line@3.1.0:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
wordwrap@1.0.0: {}
|
||||
|
||||
Reference in New Issue
Block a user