From 75a914d33c9ad1fd24692153cc2cacf7598d361b Mon Sep 17 00:00:00 2001 From: Ibinola Date: Tue, 2 Jun 2026 12:04:35 +0100 Subject: [PATCH] feat: add backend health module --- backend/src/app.module.ts | 2 + .../src/module/health/health.controller.ts | 29 +++++++++ backend/src/module/health/health.module.ts | 14 +++++ .../indicators/disk-health.indicator.ts | 31 +++++++++ .../indicators/redis-health.indicator.ts | 63 +++++++++++++++++++ 5 files changed, 139 insertions(+) create mode 100644 backend/src/module/health/health.controller.ts create mode 100644 backend/src/module/health/health.module.ts create mode 100644 backend/src/module/health/indicators/disk-health.indicator.ts create mode 100644 backend/src/module/health/indicators/redis-health.indicator.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 49cbfd6..e8f565b 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -10,6 +10,7 @@ import { buildWinstonOptions } from './common/logger.config'; import { LoggerMiddleware } from './common/middleware/logger.middleware'; import { DocumentsModule } from './documents/documents.module'; import { MailModule } from './mail/mail.module'; +import { HealthModule } from './module/health/health.module'; import { QueueModule } from './queue/queue.module'; import { RiskAssessmentModule } from './risk-assessment/risk-assessment.module'; import { StellarModule } from './stellar/stellar.module'; @@ -51,6 +52,7 @@ import { ConfigValidationSchema } from './config/config.validation'; UsersModule, AuthModule, DocumentsModule, + HealthModule, RiskAssessmentModule, StellarModule, VerificationModule, diff --git a/backend/src/module/health/health.controller.ts b/backend/src/module/health/health.controller.ts new file mode 100644 index 0000000..f9408bb --- /dev/null +++ b/backend/src/module/health/health.controller.ts @@ -0,0 +1,29 @@ +import { Controller, Get } from '@nestjs/common'; +import { + HealthCheck, + HealthCheckService, + TypeOrmHealthIndicator, +} from '@nestjs/terminus'; + +import { DiskHealthIndicator } from './indicators/disk-health.indicator'; +import { RedisHealthIndicator } from './indicators/redis-health.indicator'; + +@Controller('module/health') +export class HealthController { + constructor( + private readonly health: HealthCheckService, + private readonly typeOrm: TypeOrmHealthIndicator, + private readonly redis: RedisHealthIndicator, + private readonly disk: DiskHealthIndicator, + ) {} + + @Get() + @HealthCheck() + check() { + return this.health.check([ + () => this.typeOrm.pingCheck('postgres', { timeout: 1500 }), + () => this.redis.checkRedis('redis'), + () => this.disk.checkDiskSpace('disk'), + ]); + } +} diff --git a/backend/src/module/health/health.module.ts b/backend/src/module/health/health.module.ts new file mode 100644 index 0000000..f7e9c5e --- /dev/null +++ b/backend/src/module/health/health.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TerminusModule } from '@nestjs/terminus'; + +import { QueueModule } from '../../queue/queue.module'; +import { HealthController } from './health.controller'; +import { DiskHealthIndicator } from './indicators/disk-health.indicator'; +import { RedisHealthIndicator } from './indicators/redis-health.indicator'; + +@Module({ + imports: [TerminusModule, QueueModule], + controllers: [HealthController], + providers: [DiskHealthIndicator, RedisHealthIndicator], +}) +export class HealthModule {} diff --git a/backend/src/module/health/indicators/disk-health.indicator.ts b/backend/src/module/health/indicators/disk-health.indicator.ts new file mode 100644 index 0000000..a78a769 --- /dev/null +++ b/backend/src/module/health/indicators/disk-health.indicator.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { HealthIndicatorService } from '@nestjs/terminus'; +import checkDiskSpace from 'check-disk-space'; + +@Injectable() +export class DiskHealthIndicator { + constructor( + private readonly healthIndicatorService: HealthIndicatorService, + ) {} + + async checkDiskSpace(key: string) { + const indicator = this.healthIndicatorService.check(key); + const path = process.platform === 'win32' ? 'C:\\' : '/'; + const diskSpace = await checkDiskSpace(path); + const freeMb = Math.floor(diskSpace.free / 1024 / 1024); + + if (freeMb < 500) { + return indicator.down({ + path, + freeMb, + requiredFreeMb: 500, + }); + } + + return indicator.up({ + path, + freeMb, + requiredFreeMb: 500, + }); + } +} diff --git a/backend/src/module/health/indicators/redis-health.indicator.ts b/backend/src/module/health/indicators/redis-health.indicator.ts new file mode 100644 index 0000000..fc4a1fb --- /dev/null +++ b/backend/src/module/health/indicators/redis-health.indicator.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@nestjs/common'; +import { HealthIndicatorService } from '@nestjs/terminus'; +import { createClient } from 'redis'; + +import { QueueService } from '../../../queue/queue.service'; + +@Injectable() +export class RedisHealthIndicator { + constructor( + private readonly healthIndicatorService: HealthIndicatorService, + private readonly queueService: QueueService, + ) {} + + async checkRedis(key: string) { + const indicator = this.healthIndicatorService.check(key); + const connection = this.queueService.getConnectionOptions(); + const redisConnection = connection as { + host?: string; + port?: number; + password?: string; + }; + const host = redisConnection.host || '127.0.0.1'; + const port = redisConnection.port || 6379; + const client = createClient({ + socket: { + host, + port, + }, + password: redisConnection.password, + }); + + try { + await client.connect(); + const pong = await client.ping(); + + if (pong !== 'PONG') { + return indicator.down({ + host, + port, + message: `Unexpected ping response: ${pong}`, + }); + } + + return indicator.up({ + host, + port, + }); + } catch (error) { + return indicator.down({ + host, + port, + message: + error instanceof Error ? error.message : 'Redis health check failed', + }); + } finally { + if (client.isOpen) { + await client.quit().catch(() => client.disconnect()); + } else { + client.disconnect(); + } + } + } +}